使用电影推荐系统常用的英文数据集The Movies Dataset, 该数据集包含了2017年7月之前公映的45,000条电影记录,每条记录包含了该影片的电影名、语种、演员、导演组、剧情关键词、预算、票房、平均评分、评分人数等重要信息。其中评分记录为270,000名用户对上述所有电影给出的2600万个评分,评分区间为1-5。
需要数据集的留下邮箱,看到后会第一时间发送的
在介绍如何利用数据挖掘算法中的Apriori(关联规则)/collaborative filtering(协同过滤)算法来实现一个电影推荐系统。我们将通过加载数据、数据预处理、生成频繁项集和关联规则这几个步骤,最终通过关联规则生成电影推荐的列表。
任务目标:根据用户的观影记录或者喜好的电影列表,推荐相应的电影列表或者单部电影。例如,用户喜欢《Batman Returns》这部电影,要求推荐相应的电影列表
python=3.6.5
numpy=1.18.1
matplotlib=3.3.
mlxtend=0.17.2
movies_metadata.csv: 电影元数据文件,包含了45,000部电影的每部电影的各个属性信息,一共包含24个字段,特征字段说明如下:
1)文件movies_metadata.csv包含24个字段,具体信息如下:
序号 字段名 数据类型 字段描述
1 adult String 成人
2 belongs_to_collection String 归属
3 budget Integer 预算
4 genres String 流派
5 homepage String 主页
6 id Integer 编码
7 imdb_id String imdb编码
8 original_language String 初始语言
9 original_title String 初始标题
10 overview String 概述
11 popularity String 声望
12 poster_path String 宣传路径
13 production_companies String 发行公司
14 production_countries String 发行国家
15 release_date String 发布时间
16 revenue Integer 收入
17 runtime Integer 运行
18 spoken_languages String 语言
19 status String 状态
20 tagline String 标题
21 title String 题目
22 video String 视频
23 vote_average Integer 平均投票
24 vote_count Integer 投票数
ratings_small.csv: 评分数据文件,包含700多个用户对9000多部电影的评分数据,一共包含以下4个字段:
2)文件ratings_small.csv包含4个字段,具体信息如下:
序号 字段名 数据类型 字段描述
1 userId Integer 用户编码
2 movieId Integer imdb编码
3 rating Integer 评级
4 timestamp Integer 时间戳
导入需要的包
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
读取电影及评分数据
movies_df = pd.read_csv('movies_metadata.csv')
ratings_df = pd.read_csv('ratings_small.csv')
获得movies_df的尺寸
movies_df.shape
(45466, 24)
获取movies_df的前五行数据
movies_df.head()
ratings_df.shape
(100004, 4)
获取ratings_df的前五行数据
ratings_df.head(5)
清洗movies_metadata.csv和ratings_small.csv的数据:缺失值处理,数据去重。movies_metadata.csv缺少评分数据,ratings_small.csv缺少电影名数据,因此我们需要合并两张表的数据生成所需的DataFrame
movies_df = movies_df[['title', 'id']] #截取title和id这两列的数据
movies_df.dtypes #查看每列的数据类型
删掉timestamp列的数据
ratings_df.drop(['timestamp'], axis=1, inplace=True)
np.where(pd.to_numeric(movies_df['id'], errors='coerce').isna())
结果:(array([19730, 29503, 35587], dtype=int64),)
pd.to_numeric将id列的数据由字符串转为数值类型,不能转换的数据设置为NaN。
np.where返回满足()内条件的数据所在的位置。返回缺失值的位置,其中isna() 对于NaN返回True,否则返回False
movies_df.iloc[[19730, 29503, 35587]]
movies_df['id'] = pd.to_numeric(movies_df['id'], errors='coerce')
movies_df.drop(np.where(movies_df['id'].isna())[0], inplace=True)
movies_df.shape
(45463, 2)
movies_df.duplicated(['id', 'title']).sum() #返回重复项总数
30
movies_df.drop_duplicates(['id'], inplace=True) #数据去重
ratings_df.duplicated(['userId', 'movieId']).sum()
0
对于movies_df的id列进行类型转换
movies_df['id'] = movies_df['id'].astype(np.int64)
movies_df.dtypes
将左边的dataframe的movieId和右边的Dataframe的id进行对齐合并成新的Dataframe
ratings_df = pd.merge(ratings_df, movies_df, left_on='movieId', right_on='id')
去掉多余的id列
ratings_df.drop(['id'], axis=1, inplace=True)
ratings_df.head(10)
有评分记录的电影的个数
len(ratings_df['title'].unique())
2794
统计每部电影的评分记录的总个数
ratings_count = ratings_df.groupby(['title'])['rating'].count().reset_index()
列的字段重命名
ratings_count = ratings_count.rename(columns={'rating':'totalRatings'})
ratings_count.head(5)
ratings_total = pd.merge(ratings_df,ratings_count, on='title', how='left')
ratings_total.head(5)
数据分析以截取合适的数据,获得关于totalRatings字段的统计信息
ratings_count['totalRatings'].describe()
ratings_count.hist()
ratings_count['totalRatings'].quantile(np.arange(.6,1,0.01))
由上述数据分析可知,21%的电影的评分记录个数超过20个,选取总评个数超过阈值的电影评分数据
votes_count_threshold = 20
ratings_top = ratings_total.query('totalRatings > @votes_count_threshold')
ratings_top.head(10)
ratings_top.isna().sum()
ratings_top.duplicated(['userId','title']).sum()
140
只保留每个用户对每个电影的一条评分记录
ratings_top = ratings_top.drop_duplicates(['userId','title'])
ratings_top.duplicated(['userId','title']).sum()
调整表样式
df_for_apriori = ratings_top.pivot(index='userId',columns='title',values='rating')
df_for_apriori.head(5)
缺失值填充0,有效评分规则,1表示有效,0表示无效,并且对每个数据应用上述规则
df_for_apriori = df_for_apriori.fillna(0)
def encode_units(x):
if x <= 0:
return 0
if x > 0:
return 1
df_for_apriori = df_for_apriori.applymap(encode_units)
df_for_apriori.head(5)
例子:啤酒与尿布:
沃尔玛超市在分析销售记录时,发现了啤酒与尿布经常一起被购买,于是他们调整了货架将两者放在了一起,结果真的提升了啤酒的销量。 原因解释: 爸爸在给宝宝买尿布的时候,会顺便给自己买点啤酒?
概述:
Apriori算法是一种最有影响力的挖掘布尔关联规则的频繁项集的算法,其命名Apriori源于算法使用了频繁项集性质的先验(Prior)知识。接下来我们将以超市订单的例子理解关联分析相关的重要概念: Support(支持度)、Confidence(置信度)、Lift(提升度)。
Support(支持度):指某事件出现的概率,在本例中即指某个商品组合出现的次数占总次数的比例。例:Support(‘Bread’) = 4/5 = 0.8 Support(‘Milk’) = 4/5 = 0.8
Support(‘Bread+Milk’) = 3/5 = 0.6
Confidence(置信度):本质上是个条件概率,即当购买了商品A的前提下,购买商品B的概率。例:Confidence(‘Bread’—> ‘Milk’) = Support(‘Bread+Milk’)/ Support(‘Bread’) = 0.6/0.8 = 0.75
Lift(提升度): 指商品A的出现,对商品B的出现的概率的提升程度。
Lift(A->B) = Confidence(A, B) / Support(B)例:Lift(‘Bread’—> ‘Milk’) = 0.75/0.8 = 0.9375
对于Lift(提升度)有三种情况:
Lift(A->B)>1: 代表A对B的出现概率有提升。
Lift(A->B)=1: 代表A对B的出现概率没有提升,也没有下降。
Lift(A->B)<1: 代表A对B的出现概率有下降效果。
原理:
该算法挖掘关联规则的过程,即是**查找频繁项集(frequent itemset)**的过程:
频繁项集:支持度大于等于最小支持度(Min Support)阈值的项集。
非频繁集:支持度小于最小支持度的项集。
流程:
K = 1, 计算K项集的支持度;
筛选掉小于最小支持度的项集;
如果项集为空,则对应K-1项集的结果为最终结果。否则K = K+1重复2-3步
导入关联规则算法相关包
from mlxtend.frequent_patterns import apriori
from mlxtend.frequent_patterns import association_rules
生成符合条件的频繁项集,并降序排列的频繁项集
frequent_itemsets = apriori(df_for_apriori, min_support=0.10, use_colnames=True)
frequent_itemsets.sort_values('support', ascending=False)
生成关联规则,只保留lift>1的部分
rules = association_rules(frequent_itemsets, metric="lift", min_threshold=1)
rules.sort_values('lift', ascending=False)
all_antecedents = [list(x) for x in rules['antecedents'].values]
desired_indices = [i for i in range(len(all_antecedents)) if len(all_antecedents[i])==1 and all_antecedents[i][0]=='Batman Returns']
apriori_recommendations=rules.iloc[desired_indices,].sort_values(by=['lift'],ascending=False)
apriori_recommendations.head()
apriori_recommendations_list = [list(x) for x in apriori_recommendations['consequents'].values]
print("Apriori Recommendations for movie: Batman Returns\n")
for i in range(5):
print("{0}: {1} with lift of {2}".format(i+1,apriori_recommendations_list[i],apriori_recommendations.iloc[i,6]))
结果说明:给出用户观看过的电影记录:《Batman Returns》,我们选出了lift(支持度)降序排列的前五条关联规则,给出了对应的推荐列表。
apriori_single_recommendations = apriori_recommendations.iloc[[x for x in range(len(apriori_recommendations_list)) if len(apriori_recommendations_list[x])==1],]
apriori_single_recommendations_list = [list(x) for x in apriori_single_recommendations['consequents'].values]
print("Apriori single-movie Recommendations for movie: Batman Returns\n")
for i in range(5):
print("{0}: {1}, with lift of {2}".format(i+1,apriori_single_recommendations_list[i][0],apriori_single_recommendations.iloc[i,6]))
结果说明:我们约束consequents(后件)的长度为1,选出lift降序排列的前五个关联规则(关联规则格式为前件——>后件)。对于用户观看的电影记录《Batman Returns》,即antecedents(前件),我们根据规则按照推荐程度降序给出了单部电影推荐结果。
协同过滤一般是在海量的用户中发现一小部分和你品味比较相近的,在协同过滤中,这些用户称为邻居,然后根据他们喜欢的东西组织成一个排序的目录来推荐给你。问题的重点就是怎样去寻找和你比较相似的用户,怎么将那些邻居的喜好组织成一个排序的目录给用户。
文章主要介绍的是基于user的协同过滤,理论上是要找到找到和需要被推荐的用户最相似的用户,这里我们用邻居表示与你最为相似的用户。相似用户的计算可能有很多,在实际中我们会给出一个数字K表示和你最为相似的用户。在计算相似度的时候,理论上要计算被推荐的用户与所有用户的相似度,但是当数据量比较大的时候,这样做是很费时间的 ,数据集中可能有很多用户和需要被推荐的用户是没有关系的,在计算是完全是没有必要的,所以需要物品到用户的反查表,也就是没一件物品对应的用户信息,有了这个表,就可以过滤掉很多和你没有关系的用户,减少计算量。
读取读取ratings_small.csv数据用于建模
ratings_path = "ratings_small.csv"
ratings_df = pd.read_csv(ratings_path)
ratings_df.head(5)
始的movieId并非是从0或1开始的连续值,为了便于构建其user-item矩阵,我们将重新排列movie-id
movie_id = ratings_df['movieId'].drop_duplicates()
movie_id.head()
movie_id = pd.DataFrame(movie_id)
movie_id['movieid'] = range(len(movie_id))
print(len(movie_id))
movie_id.head()
更新movieId–>movieid
ratings_df = pd.merge(ratings_df,movie_id,on=['movieId'],how='left')
ratings_df = ratings_df[['userId','movieid','rating','timestamp']] #更新movieId-->movieid
用户物品统计
n_users = ratings_df.userId.nunique()
n_items = ratings_df.movieid.nunique()
print(n_users)
print(n_items)
671
9066
# 拆分数据集
from sklearn.model_selection import train_test_split
# 按照训练集70%,测试集30%的比例对数据进行拆分
train_data,test_data =train_test_split(ratings_df,test_size=0.3)
# 训练集 用户-物品 矩阵
user_item_matrix = np.zeros((n_users,n_items))
for line in train_data.itertuples():
#print(line)
user_item_matrix[line[1]-1,line[2]] = line[3]
构建用户相似矩阵 - 采用余弦距离
from sklearn.metrics.pairwise import pairwise_distances
# 相似度计算定义为余弦距离
user_similarity_m = pairwise_distances(user_item_matrix,metric='cosine')# 每个用户数据为一行,此处不需要再进行转置
相似矩阵
user_similarity_m[0:5,0:5].round(2)
user_similarity_m_triu = np.triu(user_similarity_m,k=1) # 取得上三角数据
user_sim_nonzero = np.round(user_similarity_m_triu[user_similarity_m_triu.nonzero()],3)
np.percentile(user_sim_nonzero,np.arange(0,101,10))
mean_user_rating = user_item_matrix.mean(axis=1)
rating_diff = (user_item_matrix - mean_user_rating[:,np.newaxis])
np.newaxis作用:为mean_user_rating增加一个维度,实现加减操作
以np.array([np.abs(item_similarity_m).sum(axis=1)]是为了可以使评分在1~ 5 之间,使1~5的标准化
user_precdiction = mean_user_rating[:,np.newaxis] + user_similarity_m.dot(rating_diff) / np.array([np.abs(user_similarity_m).sum(axis=1)]).T
只取数据集中有评分的数据集进行评估
from sklearn.metrics import mean_squared_error
from math import sqrt
prediction_flatten = user_precdiction[user_item_matrix.nonzero()]
user_item_matrix_flatten = user_item_matrix[user_item_matrix.nonzero()]
error_train = sqrt(mean_squared_error(prediction_flatten,user_item_matrix_flatten)) # 均方根误差计算
print('训练集预测均方根误差:',error_train)
test_data_matrix = np.zeros((n_users,n_items))
for line in test_data.itertuples():
test_data_matrix[line[1]-1,line[2]-1]=line[3]
预测矩阵
rating_diff = (test_data_matrix - mean_user_rating[:,np.newaxis]) # np.newaxis作用:为mean_user_rating增加一个维度,实现加减操作
user_precdiction = mean_user_rating[:,np.newaxis] + user_similarity_m.dot(rating_diff) / np.array([np.abs(user_similarity_m).sum(axis=1)]).T
只取数据集中有评分的数据集进行评估
prediction_flatten = user_precdiction[user_item_matrix.nonzero()]
user_item_matrix_flatten = user_item_matrix[user_item_matrix.nonzero()]
error_test = sqrt(mean_squared_error(prediction_flatten,user_item_matrix_flatten)) # 均方根误差计算
print('测试集预测均方根误差:',error_test)
协同过滤算法: collaborative filtering.
关联规则算法: Apriori.