跳至主要内容

流媒体平台推荐系统教程

在本 Python 教程中,探索主流流媒体平台的电影数据并构建一个推荐系统。
· 10分钟 阅读

使用 AI 探索

随着“刷剧”文化兴起,加之 Netflix、Prime Video、Hulu 和 Disney+ 等服务的普及,用户正以极快的速度消费影视内容。这些新平台中的一部分(如 Hulu 和 YouTube TV)还提供体育、演唱会/巡演以及新闻频道等活动的直播功能。而像 Netflix 这样的流媒体平台目前尚未引入直播。

流媒体平台为用户带来更灵活的观影体验:随时、随地、任意设备,想看就看。凭借海量的剧集与电影内容,这类服务尤其能够吸引年轻与注重时尚的受众。用户还可以在自己合适的时间补看错过的节目。在本教程中,您将分析 Netflix、Prime Video、Hulu 和 Disney+ 的电影数据,并尝试理解其受众。

DataCamp 助您打造 Python 职业技能横幅

理解数据集

本数据集仅包含 Netflix、Prime Video、Hulu 和 Disney+ 等流媒体平台上可观看的电影。您可以从 Kaggle 此处下载。

下面详细说明各数据字段:

  • ID:每条记录的唯一 ID。
  • Title:电影名称
  • Year:电影发行年份。
  • Age:目标年龄分级
  • IMDb:电影的 IMDb 评分。
  • Rotten Tomatoes:烂番茄百分比
  • Netflix:是否在 Netflix 上可观看
  • Hulu:是否在 Hulu 上可观看
  • Prime Video:是否在 Prime Video 上可观看
  • Disney+:是否在 Disney+ 上可观看
  • Type:电影或电视节目
  • Directors:导演姓名
  • Genres:类型
  • Country:出品国家/地区
  • Language:语言
  • Runtime:片长

让我们导入必要的模块并加载数据集:

 # Import required libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.feature_extraction.text import TfidfVectorizer    
from nltk.tokenize import RegexpTokenizer
import numpy as np
from sklearn import preprocessing
from scipy.sparse import hstack
import pandas_profiling
# Load dataset
df = pd.read_csv("tvshow.csv")
df=df.iloc[:,1:] # removing in unnamed index column
df.head()
 Unnamed: 0IDTitleYearAgeIMDbRotten TomatoesNetflixHuluPrime VideoDisney+TypeDirectorsGenresCountryLanguageRuntime
001Inception201013+8.887%10000Christopher NolanAction,Adventure,Sci-Fi,ThrillerUnited States,United KingdomEnglish,Japanese,French148.0
112The Matrix199918+8.787%10000Lana Wachowski,Lilly WachowskiAction,Sci-FiUnited StatesEnglish136.0
223Avengers: Infinity War201813+8.584%10000Anthony Russo,Joe RussoAction,Adventure,Sci-FiUnited StatesEnglish149.0
334Back to the Future19857+8.596%10000Robert ZemeckisAdventure,Comedy,Sci-FiUnited StatesEnglish116.0
445The Good, the Bad and the Ugly196618+8.897%10100Sergio LeoneWesternItaly,Spain,West GermanyItalian161.0
# Show initial information about the dataset
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 16744 entries, 0 to 16743
Data columns (total 16 columns):
 #   Column           Non-Null Count  Dtype  
---  ------           --------------  -----  
 0   ID               16744 non-null  object
 1   Title            16744 non-null  object
 2   Year             16744 non-null  int64  
 3   Age              7354 non-null   object
 4   IMDb             16173 non-null  float64
 5   Rotten Tomatoes  5158 non-null   object
 6   Netflix          16744 non-null  int64  
 7   Hulu             16744 non-null  int64  
 8   Prime Video      16744 non-null  int64  
 9   Disney+          16744 non-null  int64  
 10  Type             16744 non-null  int64  
 11  Directors        16018 non-null  object
 12  Genres           16469 non-null  object
 13  Country          16309 non-null  object
 14  Language         16145 non-null  object
 15  Runtime          16152 non-null  float64
dtypes: float64(2), int64(6), object(8)
memory usage: 2.0+ MB
df.Type.unique()
array([0])

处理缺失值

在本节中,您将使用 isnull() 函数处理缺失值。示例如下:

 #Finding Missing values in all columns
miss = pd.DataFrame(df.isnull().sum())
miss = miss.rename(columns={0:"miss_count"})
miss["miss_%"] = (miss.miss_count/len(df.ID))*100
miss
 misscountmiss%
ID00.000000
Title00.000000
Year00.000000
Age939056.079790
IMDb5713.410177
Rotten Tomatoes1158669.194935
Netflix00.000000
Hulu00.000000
Prime Video00.000000
Disney+00.000000
Type00.000000
Directors7264.335882
Genres2751.642379
Country4352.597946
Language5993.577401
Runtime5923.535595

可以看到,Age 与 Rotten Tomatoes 两个变量的缺失率均超过 50%,这值得警惕。

接下来按如下步骤处理缺失值:

  • 删除缺失率超过 50% 的列
  • 在 IMDb、Directors、Genres、Country、Language 和 Runtime 列中删除 NA
  • 重置索引
  • 将 Year 列转换为 object 类型
# Dropping values with missing % more than 50%
df.drop(['Rotten Tomatoes', 'Age'], axis = 1, inplace=True)
# Dropping Na's from the following columns
df.dropna(subset=['IMDb','Directors', 'Genres', 'Country', 'Language', 'Runtime'],inplace=True)
df.reset_index(inplace=True,drop=True)
# Converting into object type
df.Year = df.Year.astype("object")
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 15233 entries, 0 to 15232
Data columns (total 14 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   ID           15233 non-null  object
 1   Title        15233 non-null  object
 2   Year         15233 non-null  object
 3   IMDb         15233 non-null  float64
 4   Netflix      15233 non-null  int64  
 5   Hulu         15233 non-null  int64  
 6   Prime Video  15233 non-null  int64  
 7   Disney+      15233 non-null  int64  
 8   Type         15233 non-null  int64  
 9   Directors    15233 non-null  object
 10  Genres       15233 non-null  object
 11  Country      15233 non-null  object
 12  Language     15233 non-null  object
 13  Runtime      15233 non-null  float64
dtypes: float64(2), int64(5), object(7)
memory usage: 1.6+ MB

分布图

您可以使用 seaborn 的 distplot() 函数查看 Year 列的分布。让我们绘制电影年份的分布图。

#checking Distribution of years
plt.figure(figsize=(20,5))
sns.distplot(df['Year'])
plt.show()
电影年份分布图

该图展示了电影出品年份的分布。可以解读为,大多数电影集中在 2000 至 2020 年间。接着绘制 IMDb 评分分布图。

# Distribution of IMDb Rating
plt.figure(figsize=(20,5))
sns.distplot(df['IMDb'])
plt.show()
IMDb 评分分布图

上面的分布图略有偏斜。可以解读为多数电影的 IMDb 均值约为 6.5。

再来绘制电影片长分布图。

# Distribution of runtime
sns.distplot(df['Runtime'])
plt.show()
电影片长分布图

从上图可解读为,电影的平均片长大多落在 80 至 120 分钟之间。

各流媒体平台的影片分布

本节将查看按平台划分的影片分布。首先需要创建一个 m_cnt() 函数用于统计指定平台的影片数。随后可使用饼图绘制并了解各平台的占比。

# A function to calculate the movies in different Streaming platforms
def m_cnt(plat, count=False):
    if count==False:
        print('Platform {} Count: {}'. format(plat, df[plat].sum()))
    else:
        return df[plat].sum()
# Let's see count of movies/shows of each streaming platform
m_cnt('Netflix')
m_cnt('Hulu')
m_cnt('Prime Video')
m_cnt('Disney+')
Platform Netflix Count: 3152
Platform Hulu Count: 848
Platform Prime Video Count: 11289
Platform Disney+ Count: 542
# Movies on each platform
lab = 'Prime Video','Netflix', 'Hulu', 'Disney'
s = [m_cnt('Prime Video', count=True),
     m_cnt('Netflix', count=True),
     m_cnt('Hulu', count=True),
     m_cnt('Disney+', count=True)]

explode = (0.1, 0.1, 0.1, 0.1)

#plotting
fig1, ax1 = plt.subplots()
ax1.pie(s,
       labels = lab,
       autopct = '%1.1f%%',
       explode = explode,
       shadow = True,
       startangle = 100)

ax1.axis = ('equal')
plt.show()
流媒体平台

从上图可见,Prime Video 以 71% 的占比承载了最多片名,Netflix 承载了 20%。Disney+ 与 Hulu 的占比最低,分别为 5.4% 与 3.4%。

按类型划分的影片分布

本节将查看按类型划分的影片分布。首先需要准备数据:同一单元格内可能包含多个类型,您可以使用 split()、apply() 和 stack() 函数处理。split() 用逗号分隔形成列表,apply(pd.Series,1) 将每个类型展开为多列,stack() 再将其堆叠回单列。

完成这三步后,新生成的 Genres 列会与原数据集拼接,便可开始绘图。您可以用 value_counts() 展示电影数量最多的前 10 个类型,并使用 pandas 的 plot() 进行可视化。

 ##split the genres by ',' & then stack it one after the other for easy analysis.
g = df['Genres'].str.split(',').apply(pd.Series, 1).stack()
g.index = g.index.droplevel(-1)
# Assign name to column
g.name = 'Genres'
# delete column
del df['Genres']
# join new column with the existing dataframe
df_genres = df.join(g)
# Count of movies according to genre
plt.figure(figsize=(15,5))
sns.countplot(x='Genres', data=df_genres)
plt.xticks(rotation=90)
plt.show()
电影类型

从上图可见,电影最常见的类型是剧情(Drama)与喜剧(Comedy)。

按国家/地区划分的影片分布

本节将查看按国家/地区划分的影片分布。首先需要准备数据:同一单元格内可能包含多个国家/地区,可使用 split()、apply() 和 stack() 处理。split() 用逗号分隔形成列表,apply(pd.Series,1) 将每个国家/地区展开为多列,stack() 再将其堆叠回单列。

完成后,新 Country 列会与原数据集拼接,便可绘图。您可以用 value_counts() 展示电影数量最多的前 10 个国家/地区,并使用 pandas 的 plot() 可视化。

 # Split the Country by ',' & then stack it one after the other for easy analysis.
c = df['Country'].str.split(',').apply(pd.Series, 1).stack()
c.index = c.index.droplevel(-1)
# Assign name to column
c.name = 'Country'
# delete column
del df['Country']
# join new column with the existing dataframe
df_country = df.join(c)
# plotting top 10 country and movie count
df_country['Country'].value_counts()[:10].plot(kind='bar',figsize=(15,5))
plt.show()
电影出品国家/地区

上图显示,大多数电影产自美国。

按语言划分的影片分布

本节将查看按语言划分的影片分布。首先需要准备数据:同一单元格可能包含多种语言,您可以使用 split()、apply() 和 stack() 处理。split() 用逗号分隔形成列表,apply(pd.Series,1) 将每种语言展开为多列,stack() 再将其堆叠回单列。

完成这三步后,新 Language 列会与原数据集拼接,便可绘图。您可以用 value_counts() 展示电影数量最多的前 10 种语言,并使用 pandas 的 plot() 进行可视化。

 # perform stacking operation on language column
l = df['Language'].str.split(',').apply(pd.Series,1).stack()
l.index = l.index.droplevel(-1)
# Assign name to column
l.name = 'Language'
# delete column
del df['Language']
# join new column with the existing dataframe
df_language = df.join(l)
# plotting top 10 Language and movie count
df_language['Language'].value_counts()[:10].plot(kind='bar',figsize=(15,3))
plt.show()
电影语言分布图

从上图可以得出,大多数电影使用英语。

各平台的 IMDb 评分分布

本节将绘制按平台划分的 IMDb 评分分布。为得到这些结果,需要使用 melt() 函数并绘制 FacetGrid 图。melt() 会将宽表转换为长表。示例如下:

 # melting platform columns to create visualization
df2 = pd.melt(df, id_vars=["ID","Title","Year","IMDb","Type","Runtime"], var_name="platform")
df2 = df2[df2.value==1]
df2.drop(columns=["value"],axis=1,inplace=True)
# Distribution of IMDB rating in different platform
g = sns.FacetGrid(df2, col = "platform")
g.map(plt.hist, "IMDb")
plt.show()
各平台 IMDb 评分分布

上图展示了各平台上的 IMDb 评分分布情况。

探索 DataCamp 的 Python 课程库横幅

各平台按年龄分组的总片长

在“处理缺失值”一节中,我们删除了 Age 列。若要查看各平台按年龄分组的总片长,需要重新加载数据并应用 melt() 函数。

# Load dataset
df = pd.read_csv("tvshow.csv")
df=df.iloc[:,1:]
df.ID = df.ID.astype("object")

# melting platform columns to create visualization
df2 = pd.melt(df, id_vars=["ID","Title","Year","Age","IMDb","Rotten Tomatoes","Type","Runtime"], var_name="platform")
df2 = df2[df2.value==1]
df2.drop(columns=["value"],axis=1,inplace=True)

重新加载数据集并完成 melt 操作后,接下来绘制不同年龄组在各流媒体平台上的总片长。

# Total of runtime in different platform
ax = sns.barplot(x="platform", y="Runtime",hue="Age", estimator=sum, data=df2)
各平台按年龄分组的总片长

上图显示,Prime Video 上 18+ 年龄组用户的总片长远高于其他平台。可以据此推断,Prime Video 的大部分内容更侧重 18+ 年龄分组。

构建推荐系统

近几年,随着 YouTube、Walmart、Netflix 等众多互联网服务的跨越式发展,推荐系统在业界产生了巨大的影响。从推荐产品/服务,到通过线上广告变现提升企业价值,再到匹配用户相关性与偏好以促成购买,推荐系统已成为我们日常网络活动中不可或缺的一环。

总体而言,推荐系统是基于数学的方法框架,面向终端用户推荐与其需求和偏好相关的产品/服务。例如,看什么电影、读哪些文章、买什么产品、听什么音乐,或其他任何与具体领域相关的内容。构建推荐系统主要有三种方法:

  1. 基于内容的方法:基于物品的特征集合为用户或其交互过的物品建模,并向用户推荐其他相似物品。
  2. 协同过滤方法:基于用户与物品的交互相似度,或用户与用户的相似度进行建模,用于物品推荐。
  3. 混合方法:同时利用内容与协同方法以获得更佳效果。

首先对数据集进行推荐系统所需的预处理。先检查缺失值:

# Reading Data Again
df = pd.read_csv("tvshow.csv")
df=df.iloc[:,1:]
#Finding Missing values in all columns
miss = pd.DataFrame(df.isnull().sum())
miss = miss.rename(columns={0:"miss_count"})
miss["miss_%"] = (miss.miss_count/len(df.ID))*100
miss
#Dropping values with missing % more than 50%
df.drop(['Rotten Tomatoes', 'Age'], axis = 1, inplace=True)
# Dropping Na's from the following columns
df.dropna(subset=['IMDb','Directors', 'Genres', 'Country', 'Language', 'Runtime'],inplace=True)
df.reset_index(inplace=True,drop=True)
# converting into object type
df.ID = df.ID.astype("object")
df.Year = df.Year.astype("object")

您将基于余弦相似度构建两个推荐系统。

1. Using only the numerical variable
2. Using both numerical and categorical variable

仅使用数值列

步骤 1:选择数值变量

ndf = df.select_dtypes(include=['float64',"int64"])

步骤 2:使用 Min-Max 缩放器对数值变量进行缩放,以降低模型复杂度与训练时间

#importing minmax scaler
from sklearn import preprocessing

# Create MinMaxScaler Object
scaler = preprocessing.MinMaxScaler(feature_range=(0, 1))

# Create dataframe after transformation
ndfmx = pd.DataFrame((scaler.fit_transform(ndf)))

# assign column names
ndfmx.columns=ndf.columns

# Show initial 5 records
ndfmx.head()

步骤 3:使用余弦相似度计算相似度得分

现在,使用余弦相似度计算相似度得分。

# Import cosine similarity
from sklearn.metrics.pairwise import cosine_similarity

# Compute the cosine similarity
sig = cosine_similarity(ndfmx, ndfmx)

# Reverse mapping of indices and movie titles
indices = pd.Series(df.index, index=df['Title']).drop_duplicates()
indices.head()

步骤 4:编写函数,基于相似度得分获取推荐

  • 该函数接收两个参数:电影标题与相似度矩阵。它会在 indices 序列中查找标题对应的索引。
  • 获取该电影与所有电影之间的两两相似度得分。
  • 将相似度得分按降序排序并转换为列表。
  • 取相似度最高的前 10 项的得分与索引,并从数据框中返回对应的前 10 个电影标题。
def give_rec(title, sig=sig):

    # Get the index corresponding to original_title
    idx = indices[title]

    # Get the pairwise similarity scores
    sig_scores = list(enumerate(sig[idx]))

    # Sort the movies
    sig_scores = sorted(sig_scores, key=lambda x: x[1], reverse=True)

    # Scores of the 10 most similar movies
    sig_scores = sig_scores[1:11]

    # Movie indices
    movie_indices = [i[0] for i in sig_scores]

    # Top 10 most similar movies
    return df['Title'].iloc[movie_indices]
# Execute get_rec() function for getting recommendation
give_rec("The Matrix",sig = sig)

此处的推荐效果不尽如人意。造成这一结果的原因在于仅使用了电影评分、片长与平台变量。可通过引入更多信息(如类型、导演与国家/地区)进行改进。

结合数值与文本列

由于此前的推荐系统虽可运行,但推荐结果不够理想,因此我们将尝试一种更优的方法来改进结果。

df.head()

步骤 1:

将文本列合并为单列,然后使用分词器与 TF-IDF 向量器构建包含所有词语 TF-IDF 分数的稀疏矩阵。随后选择并缩放数值变量,并将其加入稀疏矩阵。在预处理时需执行以下步骤:

  • 选择所有 object 类型列并存入列表。
  • 移除 ID 与 Title 列。
  • 将所有文本/object 列用逗号连接为单列。
  • 创建分词器,去除符号与数字等不需要的元素。
  • 将文本转换为 Tfidf 向量。之后进行数值列预处理:
  • 将数值变量选入数据框
  • 使用 Min-Max 缩放器(0,1 区间)缩放数值变量
  • 使用 hstack 函数将数值变量添加到 TF-IDF 稀疏矩阵中(hstack 用于将数组按水平方向并入稀疏矩阵)。
#the function performs all the important preprocessing steps
def preprocess(df):
    #combining all text columns
    # Selecting all object data type and storing them in list
    s = list(df.select_dtypes(include=['object']).columns)
    # Removing ID and Title column
    s.remove("Title")
    s.remove("ID")
    # Joining all text/object columns using commas into a single column
    df['all_text']= df[s].apply(lambda x: ','.join(x.dropna().astype(str)),axis=1)

    # Creating a tokenizer to remove unwanted elements from our data like symbols and numbers
    token = RegexpTokenizer(r'[a-zA-Z]+')

    # Converting TfidfVector from the text
    cv = TfidfVectorizer(lowercase=True,stop_words='english',ngram_range = (1,1),tokenizer = token.tokenize)
    text_counts= cv.fit_transform(df['all_text'])

    # Aselecting numerical variables
    ndf = df.select_dtypes(include=['float64',"int64"])

    # Scaling Numerical variables
    scaler = preprocessing.MinMaxScaler(feature_range=(0, 1))

    # Applying scaler on our data and converting i into a data frame
    ndfmx = pd.DataFrame((scaler.fit_transform(ndf)))
    ndfmx.columns=ndf.columns    

    # Adding our adding numerical variables in the TF-IDF vector
    IMDb = ndfmx.IMDb.values[:, None]
    X_train_dtm = hstack((text_counts, IMDb))
    Netflix = ndfmx.Netflix.values[:, None]
    X_train_dtm = hstack((X_train_dtm, Netflix))
    Hulu = ndfmx.Hulu.values[:, None]
    X_train_dtm = hstack((X_train_dtm, Hulu))
    Prime = ndfmx["Prime Video"].values[:, None]
    X_train_dtm = hstack((X_train_dtm, Prime))
    Disney = ndfmx["Disney+"].values[:, None]
    X_train_dtm = hstack((X_train_dtm, Disney))
    Runtime = ndfmx.Runtime.values[:, None]
    X_train_dtm = hstack((X_train_dtm, Runtime))
    return X_train_dtm

步骤 2:将该函数应用于数据并创建稀疏矩阵

# Preprocessing data
mat =preprocess(df)
mat.shape

步骤 3:再次使用余弦相似度计算相似度得分

# using cosine similarity
from sklearn.metrics.pairwise import cosine_similarity

# Compute the sigmoid kernel
sig2 = cosine_similarity(mat, mat)

# Reverse mapping of indices and movie titles
indices = pd.Series(df.index, index=df['Title']).drop_duplicates()

步骤 4:从改进版系统获取推荐

give_rec("The Matrix",sig=sig2)

这一次的推荐系统效果显著优于之前的版本,说明通过添加更多相关数据(如描述性文本),基于内容的推荐系统能得到大幅提升。

结语

恭喜您阅读完本教程!

在本教程中,您对流媒体平台电影数据集进行了探索性分析。您查看了缺失值、单变量分布图,以及各流媒体平台的影片分布;还洞察了类型、国家/地区、语言、IMDb 评分与电影片长等方面。最后,您还了解了如何在 Python 中构建推荐系统。

欢迎查看 DataCamp 的Python 推荐系统教程。

DataCamp 的 Python 课程库横幅

主题