Python 图算法系列2 -电影推荐
0 说明
例子原文来自Kaggle,本文会以原文例子为主线进行展开。
主要的点在于将非结构化数据(文本描述)进行向量化,然后通过图的结构关系做推荐。
1 数据
数据可以到这里下载(Kaggle),或者也可以在这里下载(CSDN), 文件不大,大概2.3M左右。
数据一共6234行,12列。
列名 | 含义 |
---|---|
show_id | 每部电影的唯一ID |
type | N,分类变量,电影Movie Or 电视剧TV |
title | 电影或电视剧的名称,可能不唯一 |
director | 导演 |
cast | 演员名单 |
country | 原产国 |
date_added | Netflix上架日期 |
release_year | 影片发行年份 |
rating | 电影的评分 |
duration | 片长 |
listed_in | 频道/分类 |
description | 内容简介 |
2 数据分析
先读入数据看一看(DataManipulation 是自定义的函数包)
import pandas as pd
import numpy as np
import DataManipulation as dm # 1 读入数据
df = pd.read_csv('netflix_titles.csv')df.shape
# (6234, 12)# 2 观察元数据
df_meta = dm.view_df_varattr(df)
可以看到,因为变量主要都是非结构化的,例如description就是文本型的,因此基于表结构的处理方法可能没什么用。
2.1数据分析可视化
以下是用pyecharts 进行简单的一些可视化,先占个位,后续看看需要增加哪些。
2.1.1 时间轴变化函数(单数据源)
from pyecharts import options as opts
from pyecharts.charts import Bar, Timeline
from pyecharts.commons.utils import JsCode
# ---封装函数
# timerange: 2001,2002 ...
# xcols: colA, colB
# filename: xxx.html
# path = ./xxx/xxx/
def Draw1SourceCategoryViaTime(df, source_name,timerange,tcol,xcols, title,filename, path='./'):tl = Timeline()timerange = [int(x) for x in timerange]for i in timerange:# df1_summary是按照年份汇总的数据tem_dict = dict(df[df[tcol] == i])vals = []for x in xcols:vals.append(int(tem_dict[x]))bar = (Bar().add_xaxis(xcols).add_yaxis(source_name, vals)# .add_yaxis("商家B", vals2).set_global_opts(title_opts=opts.TitleOpts("{0}{1}".format(i,title)),graphic_opts=[opts.GraphicGroup(graphic_item=opts.GraphicItem(rotation=JsCode("Math.PI / 4"),bounding="raw",right=100,bottom=110,z=100,),children=[opts.GraphicRect(graphic_item=opts.GraphicItem(left="center", top="center", z=100),graphic_shape_opts=opts.GraphicShapeOpts(width=400, height=50),graphic_basicstyle_opts=opts.GraphicBasicStyleOpts(fill="rgba(0,0,0,0.3)"),),opts.GraphicText(graphic_item=opts.GraphicItem(left="center", top="center", z=100),graphic_textstyle_opts=opts.GraphicTextStyleOpts(text="{0}{1}".format(i,title),font="bold 26px Microsoft YaHei",graphic_basicstyle_opts=opts.GraphicBasicStyleOpts(fill="#fff"),),),],)],))tl.add(bar, "{}".format(i))tl.render(path+filename)print('File Saved To ', path+filename)return True# ------------------ 调用该函数的代码
# -> TimeLine 可视化
## 制作汇总数据
keep_cols = ['year','month','type']
df1 = df[keep_cols].dropna()
df1['YearMonth'] = df1['year'].apply(int).apply(str) + df1['month'].apply(int).apply(str).apply(lambda x: '0'+x if len(x) ==1 else x)df1_summary = df1.groupby(['YearMonth','type']).size().unstack()
df1_summary1 = df1.groupby(['year', 'type']).size().unstack().fillna(0).reset_index()from draw_func1 import Draw1SourceCategoryViaTime
## Time Pyecharts
source_name = 'Netflix'
timerange = range(2010,2020)
tcol = 'year'
xcols = ['Movie','TV Show']
title = '电影/电视剧数量'
filename= 'Netflix Movies.html'Draw1SourceCategoryViaTime(df1_summary1, source_name, timerange,tcol,xcols,title,filename)------------------------------
In [90]: df1_summary1.head()
Out[90]:
type year Movie TV Show
0 2008.0 1.0 1.0
1 2009.0 2.0 0.0
2 2010.0 1.0 0.0
3 2011.0 13.0 0.0
4 2012.0 4.0 3.0
效果:
2.1.2 水平轴滑动条
如果只是单一产品的时间轴展示,用这个比时间轴变化函数要好(后者更适合与多数据源,多产品的时间比较)
from pyecharts import options as opts
from pyecharts.charts import Bardef SliderBar(source_name,x_str_list, y_val_list, title, filename, path='./'):x_str_list = [str(x) for x in x_str_list]y_val_list = [int(x) for x in y_val_list]c = (Bar().add_xaxis(x_str_list).add_yaxis(source_name, y_val_list).set_global_opts(title_opts=opts.TitleOpts(title=title),datazoom_opts=opts.DataZoomOpts(),).render(path+filename))print('File Saved To ', path+filename)return True
# -------- 调用代码
from draw_func2 import SliderBar
source_name = 'Movies&TVs'
x_str_list = df1_summary1['year'].apply(int)
y_val_list = df1_summary1['Movie'] + df1_summary1['TV Show']
title ='Netflix Moview&TVs By Year'
filename = 'Netflix Movies1.html'SliderBar(source_name,x_str_list,y_val_list,title, filename)
效果:
2.1.3 堆叠柱状图
这个可能会更加常用。
from pyecharts import options as opts
from pyecharts.charts import Bardef Stacked2SouceBar(source_name1, source_name2, x_str_list, y_val_list1, y_val_list2, title, filename, path='./'):x_str_list = [str(x) for x in x_str_list]y_val_list1 = [int(x) for x in y_val_list1]y_val_list2 = [int(x) for x in y_val_list2]c = (Bar().add_xaxis(x_str_list).add_yaxis(source_name1, y_val_list1, stack="stack1").add_yaxis(source_name2, y_val_list2, stack="stack1").set_series_opts(label_opts=opts.LabelOpts(is_show=False)).set_global_opts(title_opts=opts.TitleOpts(title=title)).render(path+filename))print('File Saved To ', path+filename)return True
# ---------------------- 调用
from draw_func3 import Stacked2SouceBar
source_name1 = 'Movies'
source_name2 = 'TV Shows'
x_str_list = df1_summary1['year'].apply(int)
y_val_list1 = df1_summary1['Movie']
y_val_list2 = df1_summary1['TV Show']
title = 'Netflix Moview&TVs By Year'
filename = 'Netflix Movies2.html'Stacked2SouceBar(source_name1,source_name2,x_str_list,y_val_list1,y_val_list2, title, filename)
效果:
2.1.4 柱状打分图
这里暂时用来看缺失率(用来给变量的重要性打分应该也不错)。
# DataSet 数据集 可筛选Bar
from pyecharts import options as opts
from pyecharts.charts import Bar# columns
# matlist |-> insert columns to head
# score
# min, max (of score)
# path ,filname
# InfOrFloat
# titledef try_to_trans(x, IntOrFloat='int'):try:if IntOrFloat.lower() == 'int':res = int(x)else:res = float(x)except:res = str(x)return resdef ScoreBar(x_str_list, matlist, scorename, valname, catename,minscore, maxscore,filename, path='./',title='', IntOrFloat='int'):headers = [str(x) for x in x_str_list]rows = []for i in range(len(matlist)):tem_row = [try_to_trans(x, IntOrFloat=IntOrFloat) for x in matlist[i]]rows.append(tem_row)rows.insert(0,headers)c = (Bar().add_dataset(source=rows).add_yaxis(series_name="",yaxis_data=[],encode={"x": valname, "y": catename},label_opts=opts.LabelOpts(is_show=False),).set_global_opts(title_opts=opts.TitleOpts(title=title),xaxis_opts=opts.AxisOpts(name=valname),yaxis_opts=opts.AxisOpts(type_="category"),visualmap_opts=opts.VisualMapOpts(orient="horizontal",pos_left="center",min_=minscore,max_=maxscore,range_text=["High Score", "Low Score"],dimension=0,range_color=["#FF0000", "#00FF7F"],),).render(path+filename))print('File Saved To ', path+filename)return True# ----------------------调用
from draw_func6 import ScoreBar
score_bar_data = df_meta[['missing_num', 'missing_ratio', 'varname']]
score_bar_data['missing_score'] = (1-score_bar_data['missing_ratio'])*100
score_bar_data = score_bar_data.sort_values(['missing_num'], ascending=False)x_str_list = ['missing_score','missing_num','varname']
matlist = score_bar_data[x_str_list].values.tolist()
scorename = 'missing_score'
valname = 'missing_num'
catename = 'varname'
minscore = 0
maxscore = 100
filename = 'Netflix Movies3.html'
title = '变量缺失情况'
ScoreBar(x_str_list, matlist, scorename, valname, catename,minscore, maxscore,filename, path='./',title=title, IntOrFloat='int')
效果:
可以看到,导演缺失的比较多,不过总体缺失情况还好。
2.1.5 4X4饼图
用来显示交叉表的结果,暂时实现4X4饼图的效果。
# 数据
df_crosstab = pd.crosstab(df['rating'], df['release_year']).reset_index()
# ---
In [210]: df_crosstab
Out[210]:
release_year rating 1925 1942 1943 ... 2017 2018 2019 2020
0 G 0 0 0 ... 2 1 0 0
1 NC-17 0 0 0 ... 0 1 0 0
2 NR 0 0 1 ... 4 5 2 0
3 PG 0 0 0 ... 11 30 7 0
4 PG-13 0 0 0 ... 22 19 10 0
5 R 0 0 0 ... 47 24 24 0
6 TV-14 0 0 1 ... 279 291 211 5
7 TV-G 0 1 0 ... 25 15 25 1
8 TV-MA 0 0 0 ... 400 491 412 17
9 TV-PG 1 1 1 ... 105 103 83 0
10 TV-Y 0 0 0 ... 19 30 29 1
11 TV-Y7 0 0 0 ... 36 26 27 0
12 TV-Y7-FV 0 0 0 ... 7 27 13 1
13 UR 0 0 0 ... 0 0 0 0[14 rows x 73 columns]# --- 4X4饼图函数
def Pie4CrossTab(x_str_list,y_str_list, matlist, x_4dim_list, y_4dim_list,filename, path='./',title='', IntOrFloat='int'):x_str_list = [str(x) for x in x_str_list]y_str_list = [str(x) for x in y_str_list]col_index = [x_str_list.index(x) for x in y_4dim_list]row_index = [y_str_list.index(y) for y in x_4dim_list]rows = []for i in range(len(matlist)):if i in row_index:tem_row = []for j in range(len(x_str_list)):if j == 0:tem_row.append(str(matlist[i][j]))elif j in col_index:tem_row.append(try_to_trans(matlist[i][j], IntOrFloat=IntOrFloat))rows.append(tem_row)headers = [x_str_list[x] for x in col_index]headers.insert(0, x_str_list[0])rows.insert(0, headers)c = (Pie().add_dataset(source=rows).add(series_name= x_4dim_list[0],data_pair=[],radius=60,# 左上center=["25%", "30%"],encode={"itemName": headers[0], "value": y_4dim_list[0]},).add(series_name=x_4dim_list[1],data_pair=[],radius=60,# 右上center=["75%", "30%"],encode={"itemName": headers[0], "value": y_4dim_list[1]},).add(series_name=x_4dim_list[2],data_pair=[],radius=60,# 左下center=["25%", "75%"],encode={"itemName": headers[0], "value": y_4dim_list[2]},).add(series_name=x_4dim_list[3],data_pair=[],radius=60,# 右上center=["75%", "75%"],encode={"itemName": headers[0], "value": y_4dim_list[3]},).set_global_opts(title_opts=opts.TitleOpts(title=title),legend_opts=opts.LegendOpts(pos_left="30%", pos_top="2%"),).render(path+filename))print('File Saved To ', path+filename)return True
# ---- 调用
x_str_list = list(df_crosstab.columns)
y_str_list = list(df_crosstab['rating'])
matlist = df_crosstab.values.tolist()
x_4dim_list = ['TV-14', 'TV-MA', 'TV-PG', 'TV-Y7']
y_4dim_list = ['2015','2016','2017','2018']
filename = 'Netflix Movies4.html'
title = '评级与年份情况'Pie4CrossTab(x_str_list,y_str_list, matlist, x_4dim_list, y_4dim_list,filename, path='./',title=title, IntOrFloat='int')
效果:
3 正式开始
以下的部分会顺着例子的主线进行介绍,Kaggle原版例子在这里
3.1 要做什么?
基于使用Adamic Adar measure建立的图推荐电影。评分越高,匹配度越高。
Adamic Adar measure= Frequency-Weighted Common Neighbors
【共同邻居的频数加权距离】
当我们计算两个相同邻居的数量的时候,其实每个邻居的“重要程度”都是不一样的,我们认为这个邻居的邻居数量越少,就越凸显它作为“中间人”的重要性,毕竟一共只认识那么少人,却恰好是x,y的好朋友。
3.2 对于描述(Description)变量的处理思考
- 1 第一个想法
- 为了把描述变量纳入模型,使用KMeans对TF-IDF特征化后的向量进行聚类。如果两部电影的描述一致,那么这两部电影视为一个节点。
- 如果一个组中的电影数量越少,那么它们的联系越强
结论:不work, 因为cluster非常不平衡
2 第二个想法
- 为每部电影描述计算TF-IDF矩阵,选择前5个相似描述,并建立一个相似节点。 参考
3 第三个想法Adamic Adar measure(AAM)
- 通过两个节点共同的邻居来描述两个点的接近程度 -> 间接度量指标
- AAM(x,y) = sum (1/ N(u)) for u be the neighbor of both x and y
- 如果x,y的邻居u有许多邻居(度比较高),那么这个u不太被计入x和y的AAM
- 反之,u没什么邻居,那么这个u对AMM的贡献比较大。
4 文本字段的向量化及聚类
4.1 文本列表的简单拆分
将 director(导演), listed_in(频道/分类), cast(演员名单) and country(原产国)这些原本是字符串的变量进行转换,变为列表。
# 将 director(导演), listed_in(频道/分类), cast(演员名单) and country(原产国)
# 这些字符串变量使用apply转换为列表,如果缺失值那么就是空列表
df['directors'] = df['director'].apply(lambda l: [] if pd.isna(l) else [i.strip() for i in l.split(",")])
df['categories'] = df['listed_in'].apply(lambda l: [] if pd.isna(l) else [i.strip() for i in l.split(",")])
df['actors'] = df['cast'].apply(lambda l: [] if pd.isna(l) else [i.strip() for i in l.split(",")])
df['countries'] = df['country'].apply(lambda l: [] if pd.isna(l) else [i.strip() for i in l.split(",")])# 数据表 -> 太长,还是不如DataTables
# from draw_func5 import FlatTable
# x_str_list = df.columns
# matlist = df.values.tolist()
# filename = 'tem.html'
# title = '对导演、频道/分类、演员名单、原产国等变量处理后的数据'# FlatTable(x_str_list, matlist, filename, title=title)
4.2 对Description进行TF-IDF
这里因为是英文,所以可以使用Sklearn进行处理。对于中文,一般使用Jieba来计算,这部分可以单独开一个专题(TF-IDF虽然简单,但是如果不能做成离线计算就没什么意义)。
这块参考原文,内容不是本次重点。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel
from sklearn.cluster import MiniBatchKMeans
# Build the tfidf matrix with the descriptions
start_time = time.time()
text_content = df['description']
vector = TfidfVectorizer(max_df=0.4, # 去掉普遍的词 # drop words that occur in more than X percent of documentsmin_df=1, # 入选词的门槛 only use words that appear at least X timesstop_words='english', # remove stop wordslowercase=True, # Convert everything to lower caseuse_idf=True, # Use idfnorm=u'l2', # Normalizationsmooth_idf=True # Prevents divide-by-zero errors)
tfidf = vector.fit_transform(text_content)
# Clustering Kmeans
k = 200
kmeans = MiniBatchKMeans(n_clusters=k)
kmeans.fit(tfidf)
centers = kmeans.cluster_centers_.argsort()[:, ::-1]
terms = vector.get_feature_names()# print the centers of the clusters
# for i in range(0,k):
# word_list=[]
# print("cluster%d:"% i)
# for j in centers[i,:10]:
# word_list.append(terms[j])
# print(word_list)request_transform = vector.transform(df['description'])
# new column cluster based on the description
df['cluster'] = kmeans.predict(request_transform)df['cluster'].value_counts().head()# 聚类结果非常不均匀
Out[13]:
44 5950
199 9
85 7
159 6
125 6
Name: cluster, dtype: int64# 另一个相似性函数
# Find similar : get the top_n movies with description similar to the target description
def find_similar(tfidf_matrix, index, top_n = 5):cosine_similarities = linear_kernel(tfidf_matrix[index:index+1], tfidf_matrix).flatten()related_docs_indices = [i for i in cosine_similarities.argsort()[::-1] if i != index]return [index for index in related_docs_indices][0:top_n]
5 构建无向图
原文中对图进行的抽象如下
图的节点和边:
- 1 点(的类别):
- 1 电影
- 2 人(导演或演员)
- 3 分类
- 4 原产国
- 5 聚类
- 6 5个最相似的描述
- 2 边
- 1 扮演(演员和电影之间)
- 2 分类(电影和分类之间)
- 3 导演(导演和电影之间)
- 4 国家(国家和电影之间)
- 5 描述(描述和电影之间)
- 6 相似性(由find_similar定义)
备注:两个电影间没有直接连接,都是通过演员,导演,描述之类的连接。
以下是我的观点:
某种程度上说,电影、人、原产国甚至聚类等的都可以视为「点」,如果把「点」理解成为认知里某个概念(Concept)的话。但是我觉得如果把谋改革概念视为点的时候最好满足两点:
1.概念之间是天然独立的,例如说人和飞机分别视为两类节点应该没有人反对。但是最好不要出现虚虚实实的混杂,例如这里的人(该概念指代一个具体的独立的实体),而聚类和描述是什么?该把他们作为相提并论的点吗。
2.模型是为了通过描述、预测来帮助人类更好的认知以及处理问题,那么把某个概念作为点是否对问题有帮助?把原产国作为电影的一个属性是不是更好呢?
关于边,一种边代表一种关系,有时候把所有边放在一起可能对存储可取(例如把图存入类似Neo4j这样的图数据库,需要时可以按照节点、边的类型和属性方便的提出来),但是对于算法不太可取。扮演、导演和投资可以把人和电影联系起来,但他们能一样吗?获取在计算连通性的时候有用,但个人感觉,如果没有特别合适的度量(定义边的计算方法),不要贸然把所有的边揉在一起。
综合起来,我的思路如下:
- 关于图,数据分为节点和关系两种。存储方法有很多种(关系型数据库 Mysql, 非关系型数据库 Mongo以及专门的图数据库Neo4j)。从实验方便性来说,可以使用Mysql这种最习惯的表结构来看,许多图属性还是使用Neo4j计算比较快,Networkx太慢了。
- 关于如何积累数据。这里有一个假设:我们能观察到的数据始终是不完整的(有些数据本来有,但是没拿到)以及包含错误。所以,不能“全信”拿到的数据,有部分需要通过推断来补全,当然推断本身又需要通过推断来管理…
一张完整的节点表:
NodeId | Attr | Val | CreateTS | UpdateTS | InferId | NodeIDInfer | AttrInfer | ValInfer | CreateTS | UpdateTS |
---|---|---|---|---|---|---|---|---|---|---|
节点ID | 属性 | 值 | 创建时间 | 更新时间 | 推断ID | 推断节点 | 推断属性 | 推断值 | 推断创建时间 | 推断更新时间 |
在使用算法时,通过计算和筛选,我们只需要这样一张表
NodeId | Attr | Val |
---|---|---|
节点ID | 属性 | 值 |
关系表在推断方面和节点表类似,但是在具体应用在某个问题时也会是简单的一张表,形式上类似一笔电商的交易:
NodeA | NodeB | Val |
---|---|---|
节点ID | 属性 | 值 |
值得注意的是,这样一张表其实只是某种关系(网),某种属性下的,例如A给B5万元。而最终的结果可能是要叠加多张网生效的。
未完待续…
后续内容:
3. 将数据构建为无向图
4. 子图分析
5. 基于图的推荐函数
6. 结果测试
Python 图算法系列2 -电影推荐相关推荐
- 基于python+django的个性化电影推荐系统设计与实现
目录 第 1 章 绪论 1 1.1 研究背景及意义 1 1.2 国内外研究现状 1 1.3 本文研究目标和研究内容 4 1.4 论文结构安排 4 第 2 章 推荐算法的研究 7 2.1 推荐算法简介 ...
- 基于Python和Tensorflow的电影推荐算法
第一步:收集和清洗数据 数据链接:https://grouplens.org/datasets/movielens/ 下载文件:ml-latest-small import pandas as pd ...
- Python 图算法系列13-cypher 查询以及模糊查询
说明 整理一些常用的查询备用,可以参考这篇文章. 查询之前要先建立索引,如果是动态的写入数据(小批量方式),那么在建库的时候就先声明索引:如果是静态的一次性删库导入,只能等数据完全导入后建立索引(2亿 ...
- Python基于用户协同过滤算法电影推荐的一个小改进
之前曾经推送过这个问题的一个实现,详见:Python基于用户协同过滤算法的电影推荐代码demo 在当时的代码中没有考虑一种情况,如果选出来的最相似用户和待测用户完全一样,就没法推荐电影了.所以,在实际 ...
- python爬取图片教程-推荐|Python 爬虫系列教程一爬取批量百度图片
Python 爬虫系列教程一爬取批量百度图片https://blog.csdn.net/qq_40774175/article/details/81273198# -*- coding: utf-8 ...
- 免费python全套视频教学-有哪些优质的Python全系列视频教程推荐,免费的收费的都可以?...
为大家推荐两本适合小白的python书籍,希望能对你有所帮助. <python编程从入门到实践> /> 本书是一本针对所有层次的Python 读者而作的Python 入门书.全书分两 ...
- 基于Python + Django + mysql的协同推荐算法的电影推荐系统
基于Python + Django + mysql的协同推荐算法的电影推荐系统 本系统一共分为前台系统功能和后台系统功能两个模块,两个模块之间虽然在表面上是相互独立的,但是在对数据库的访问上是紧密相连 ...
- Python基于修正余弦相似度的电影推荐引擎
//2022.7.15更新,经评论区提醒,更正cosine函数相关描述. 数据集下载地址:MovieLens 最新数据集 数据集包含600 名用户对 9,000 部电影应用了 100,000 个评级和 ...
- python电影推荐算法_基于Python的电影推荐算法
原标题:基于Python的电影推荐算法 第一步:收集和清洗数据 数据链接:https://grouplens.org/datasets/movielens/ 下载文件:ml-latest-small ...
- 在线电影推荐网 Python+Django+Mysql 协同过滤推荐算法在电影网站中的运用 基于用户、物品的协同过滤推荐算法 开发在线电影推荐系统 电影网站推荐系统 人工智能、大数据、机器学习开发
在线电影推荐网 Python+Django+Mysql 协同过滤推荐算法在电影网站中的运用 基于用户.物品的协同过滤推荐算法 开发在线电影推荐系统 电影网站推荐系统 人工智能.大数据.机器学习开发 M ...
最新文章
- php中对于json_decode()和json_encode()的使用方法笔记
- 网站发布问题及使用Web Deployment Projects
- 利用Eclipse/MyEclipse 实体类生成.hbm.xml文件
- java + httpclient +post请求(记录下)
- 选择排序稳定吗_最常见的四种数据结构排序算法你不知道?年末怎么跳槽涨薪...
- 程序员的.NET时代
- yum -y install与yum install有什么不同
- Android之mvp和mvc对比分析以及实际应用
- c 中=和==的区别有哪些?
- 计算机应用综合实践实验心得,综合实践活动培训心得体会范文(精选5篇)
- kylin启动netstat: n: unknown or uninstrumented protocol
- android中的多渠道打包,Android 多渠道打包简析
- (一) Qt Model/View 的简单说明
- Codeforces Round #460 (Div. 2)
- AJAX ControlToolkit学习日志-AnimationExtender控件(3)
- ai自动生成字幕软件有哪些?自动生成字幕软件推荐!
- Linux下基于UDP协议实现的聊天室项目(附源码)
- 使用editor编辑器遇到的小问题:editor.md工具栏置顶
- 盈高入网规范管理平台linux,入网引导测试和修复测试
- HTML5不支持createtouch,新手写createjs时容易遇到的坑(持续更新)