پیشنهاد فیلم مشابه بر اساس همبستگی-Correlation
سیستمهای پیشنهاد دهنده در محیط های مختلف قابل مشاهده هستند. برای مثال در music Player که بر اساس سلیقه ما پیشنهاداتی برای موسیقی ارائه میشود.در این نوشته قصد داریم به صورت ابتدایی سیستم recommender را پیادهسازی کنیم.
این سیستمها بر اساس دیتا عمل میکنند و دو روش معروف آن با نامهای زیر شناخته میشوند:
- 1- Collaborative filtering Based
- 2- Content based
در روش Collaborative based بر اساس رفتاری که سایر کاربران داشتهاند به کاربر فعلی پیشنهاداتی داده میشود.
در روش Content based بر اساس ویژگیهایی که یک ایتم دارد، پیشنهاداتی داده میشود. برای مثال اگر شخصی در فروشگاه آنلاین، دوچرخه خریداری کند، سیستم پیشنهاددهنده، کالاهای جانبی (مثلا، کلاه و …) نیز به او پیشنهاد میدهد. یا اگر فردی لباس خریداری کند، رنگهای دیگری از آن لباس نیز ممکن است به او پیشنهاد شود. یا شخصی که در سایت آمازون معمولا کتابهای علمی خریداری میکند، پیشنهادات داده شده به این شخص به احتمال زیاد در همین حوزه خواهد بود.
حال به سراغ نوتبوک میرویم و سعی میکنیم یک سیستم پیشنهاد دهنده تشکیل دهیم. در ابتدا کتابخانههای مورد نیاز را ایمپورت میکنیم:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
سپس دیتای خود را فرا میخوانیم (در بخش منابع پایان متن، دیتاها در دسترس هستند):
df1 = pd.read_csv('u_data.txt')
df1

مشاهده میشود که دیتای ما یک سری داده هستند که با اسلش(\) از یکدیگر جدا شدهاند. اما بر اساس domain knowledge میدانیم که عدد اول user_id ، عدد دوم Item_id و … هستند. کاری که باید کنیم این است که آنها را در قالب استاندارد در بیاوریم.
column_names = ['user_id', 'item_id', 'rating', 'timestamp']
df = pd.read_csv('u_data.txt', sep='\t', names=column_names)
کاری که کد بالا میکند این است که دیتاها را بر اساس t\ جداسازی (seperate) میکند.
df:

دیتاست بعدی را نیز میخوانیم:
movie_titles = pd.read_csv("Movie_Id_Titles.txt")
movie_titles.head()

با توجه به وجود یک ستون مشترک به نام item_id ، میتوانیم این دو دیتاست را در کنار هم قرار دهیم یا merge کنیم. این کار باعث میشود ستون title به دیتاست اول (u.data) افزوده شود:
df = pd.merge(df,movie_titles,on='item_id')
df.head()

میخواهیم ببینیم هر فیلم چه تعداد rate دریافت کرده است و متوسط آن چقدر بوده است. برای این کار از groupby استفاده میکنیم:
1: df.groupby('title')['rating'].mean().sort_values(ascending=False).head(15)

2:df.groupby('title')['rating'].count().sort_values(ascending=False).head()

دستور اول بالا میگوید، دیتا فریم df را در نظر بگیر و عمل groupby را روی title انجام بده و در این کار میانگین rating را در نظر بگیر. (و از بیشترین به کمترین sort کن.)
دستور دوم کار مشابهی را انجام میدهد با این تفاوت که به تعداد rating توجه میکند. (و از بیشترین به کمترین sort میکند.) حال آنچه داریم را با استفاده از pandasدر قالب دیتافریم در میآوریم:
1- بر اساس میانگین
ratings = pd.DataFrame(df.groupby('title')['rating'].mean())
ratings.head()

2- بر اساس تعداد فیدبک
ratings['num of ratings'] = pd.DataFrame(df.groupby('title')['rating'].count())
ratings.head()

حال میخواهیم اطلاعاتی در مورد دیتافریم خود به صورت بصری داشته باشیم.
plt.figure(figsize=(10,4))
sns.histplot(data=ratings, x="num of ratings")

plt.figure(figsize=(10,4))
sns.histplot(data=ratings, x="rating")

و نمودار jointplot بر حسب دو شاخص تعداد فیدبک و مقدار هر فیدبک:
sns.jointplot(x='rating',y='num of ratings',data=ratings,alpha=0.5)

شکل بالا نشان میدهد که فیلمهایی که ریتینگ دریافت نکردهاند، پرتعداد بودهاند و همچینن فیلمهای زیادی امتیازی بین 3 تا 4 دریافت کردهاند.
ساخت سیستم پیشنهاد دهنده:
حال یک دیتا فریم میسازیم که محور عمودی آن user_ID و محور افقی آن عناوین فیلمها باشد.
moviemat = df.pivot_table(index='user_id',columns='title',values='rating')
moviemat.head()

اگر فیلمی rate نداشته باشد، NaN ثبت شده است. برای مثال یوزر شماره یک، به فیلم شماره 101 امتیاز 2 را داده است. طبیعی است که هر شخص به دلیل محددیت زمان و تعداد بالای ویدئوها تنها تعدادی از فیلمها را دیده باشد. در این مسئله تلاشی برای پر کردن خانههای NaN نمیکنیم، زیرا ذات مسئله همین است. مجددا دادهها را بر اساس تعداد ریتینگ sort میکنیم:
ratings.sort_values('num of ratings',ascending=False).head(10)

حال دو فیلم را انتخاب میکنیم. یکی جنگ ستارگان (star wars) که ژانر علمی تخیلی دارد و دیگری فیلم liar liar با ژانر کمدی. از دیتافریم moviemat ستون مرتبط با این دو فیلم را جدا میکنیم:
starwars_user_ratings = moviemat['Star Wars (1977)']
starwars_user_ratings.head()

با استفاده از متود corrwith میتوانیم همبستگی یا correlation بین دو سری را بدست بیاوریم:
similar_to_starwars = moviemat.corrwith(starwars_user_ratings)
similar_to_starwars

حال تنها مقادیر NaN را حذف میکنیم و به جای سری از دیتافریم استفاده میکنیم و نام تک ستون موجود را نیز Correlation میگذاریم:
corr_starwars = pd.DataFrame(similar_to_starwars,columns=['Correlation'])
corr_starwars.dropna(inplace=True)
corr_starwars.head()

حال این Correlation را sort میکنیم:
corr_starwars.sort_values('Correlation',ascending=False).head(10)

اما این معیار صحیحی نیست. زیرا مقدار ریتینگ مهم نیست، بلکه تعداد و مقدار مهم است. مثلا فیلمی که در لیست بالا شاخص همبستگی یک دریافت کرده است ممکن است تنها یک رای (فیدبک) دریافت کرده باشد. در ابتدا تعداد ریت را به دیتافریم خود اضافه میکنیم:
corr_starwars = corr_starwars.join(ratings['num of ratings'])
corr_starwars.head()

سپس فیلمهایی انتخاب میکنیم که حداقل صد ریتینگ دریافت کرده باشند. بنابراین با استفاده از conditional matrix فیلمهایی که کمتر از صد ریتینگ دریافت کرده باشند را drop میکنیم.
corr_starwars[corr_starwars['num of ratings']>100].sort_values('Correlation',ascending=False).head()

اگر شاخص همبستگی 1- باشد یعنی بیشترین شباهت اما در جهت معکوس بین آن دو وجود دارد.
همین مراحل را برای فیلم دوم (Liar Liar) هم اجرا میکنیم:
liarliar_user_ratings = moviemat['Liar Liar (1997)']
similar_to_liarliar = moviemat.corrwith(liarliar_user_ratings)
corr_liarliar = pd.DataFrame(similar_to_liarliar,columns=['Correlation'])
corr_liarliar.dropna(inplace=True)
corr_liarliar = corr_liarliar.join(ratings['num of ratings'])
corr_liarliar[corr_liarliar['num of ratings']>100].sort_values('Correlation',ascending=False).head()

منابع:
2- کانال آموزش-DataroadMap – مونا حاتمی
3- my-github