parent
4a4b672506
commit
a839d0684b
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,347 @@
|
||||
from ast import literal_eval
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
|
||||
from sklearn.metrics.pairwise import linear_kernel, cosine_similarity
|
||||
from surprise import Dataset, Reader, SVD, accuracy
|
||||
from nltk.stem.snowball import SnowballStemmer
|
||||
from sklearn.decomposition import PCA
|
||||
from surprise.model_selection import cross_validate, train_test_split
|
||||
|
||||
|
||||
# Recommandation des films les mieux notés
|
||||
|
||||
# On récupère les data des films
|
||||
md = pd.read_csv('../Databases/movies_metadata.csv')
|
||||
|
||||
# Nettoyage des données pour le genre du film
|
||||
md['genres'] = md['genres'].fillna('[]').apply(literal_eval).apply(lambda x: [i['name'] for i in x] if isinstance(x, list) else [])
|
||||
|
||||
# On récupère le nombre de votes et la note moyenne donnée par les votes
|
||||
vote_counts = md[md['vote_count'].notnull()]['vote_count'].astype('int')
|
||||
vote_averages = md[md['vote_average'].notnull()]['vote_average'].astype('int')
|
||||
C = vote_averages.mean()
|
||||
|
||||
# On récupère le 95ème quantile, pour n'avoir que les films qui ont une meilleure note que 95% des films
|
||||
m = vote_counts.quantile(0.95)
|
||||
|
||||
# On nettoie les données de year
|
||||
md['year'] = pd.to_datetime(md['release_date'], errors='coerce').apply(lambda x: str(x).split('-')[0] if x != np.nan else np.nan)
|
||||
|
||||
# On récupère les films qui sont qualifiés par rapport aux exigences précédentes
|
||||
qualified = md[(md['vote_count'] >= m) & (md['vote_count'].notnull()) & (md['vote_average'].notnull())][['title', 'year', 'vote_count', 'vote_average', 'popularity', 'genres']]
|
||||
qualified['vote_count'] = qualified['vote_count'].astype('int')
|
||||
qualified['vote_average'] = qualified['vote_average'].astype('int')
|
||||
|
||||
# Méthode pour définir le poids de chaque critère dans le rating, utilisation de la weighted rating formula
|
||||
def weighted_rating(x):
|
||||
v = x['vote_count']
|
||||
R = x['vote_average']
|
||||
return (v/(v+m) * R) + (m/(m+v) * C)
|
||||
|
||||
# On applique le weighted rating aux films
|
||||
qualified['wr'] = qualified.apply(weighted_rating, axis=1)
|
||||
# On récupère les 250 films les mieux notés
|
||||
qualified = qualified.sort_values('wr', ascending=False).head(250)
|
||||
|
||||
# On affiche les 15 films les mieux notés
|
||||
# print(qualified.head(15))
|
||||
|
||||
|
||||
|
||||
# Recommandation des films les mieux notés d'un certain genre
|
||||
|
||||
# Nettoyage des données de genre
|
||||
s = md.apply(lambda x: pd.Series(x['genres']),axis=1).stack().reset_index(level=1, drop=True)
|
||||
s.name = 'genre'
|
||||
gen_md = md.drop('genres', axis=1).join(s)
|
||||
|
||||
# On fait la même chose qu'au-dessus, mais en sélectionnant uniquement les films dont le genre est passé en paramètres
|
||||
def build_chart(genre, percentile=0.85):
|
||||
df = gen_md[gen_md['genre'] == genre]
|
||||
vote_counts = df[df['vote_count'].notnull()]['vote_count'].astype('int')
|
||||
vote_averages = df[df['vote_average'].notnull()]['vote_average'].astype('int')
|
||||
C = vote_averages.mean()
|
||||
m = vote_counts.quantile(percentile)
|
||||
|
||||
qualified = df[(df['vote_count'] >= m) & (df['vote_count'].notnull()) & (df['vote_average'].notnull())][['title', 'year', 'vote_count', 'vote_average', 'popularity']]
|
||||
qualified['vote_count'] = qualified['vote_count'].astype('int')
|
||||
qualified['vote_average'] = qualified['vote_average'].astype('int')
|
||||
|
||||
qualified['wr'] = qualified.apply(lambda x: (x['vote_count']/(x['vote_count']+m) * x['vote_average']) + (m/(m+x['vote_count']) * C), axis=1)
|
||||
qualified = qualified.sort_values('wr', ascending=False).head(250)
|
||||
|
||||
return qualified
|
||||
|
||||
# Test sur le genre romance, affiche les 15 films de genre 'Romance' les plus aimés
|
||||
print(build_chart('Romance').head(15))
|
||||
|
||||
|
||||
|
||||
# Content based recommanders
|
||||
|
||||
# On récupère les données et on les nettoie
|
||||
links_small = pd.read_csv('../Databases/links_small.csv')
|
||||
links_small = links_small[links_small['tmdbId'].notnull()]['tmdbId'].astype('int')
|
||||
|
||||
md = md.drop([19730, 29503, 35587])
|
||||
|
||||
# On cast les id en int
|
||||
md['id'] = md['id'].astype('int')
|
||||
|
||||
# On récupère les données des films dont les ids sont dans links
|
||||
smd = md[md['id'].isin(links_small)]
|
||||
|
||||
|
||||
# Movie description based recommander
|
||||
|
||||
# On récupère les taglines et les descriptions des films
|
||||
smd['tagline'] = smd['tagline'].fillna('')
|
||||
smd['description'] = smd['overview'] + smd['tagline']
|
||||
smd['description'] = smd['description'].fillna('')
|
||||
|
||||
# Term frequency Inverse document frequency (TFIDF) is a statistical formula to convert text documents
|
||||
# into vectors based on the relevancy of the word. It is based on the bag of the words model to create
|
||||
# a matrix containing the information about less relevant and most relevant words in the document.
|
||||
# On utilise cette formule pour avoir une matrice des mots les plus intéressants pour notre IA
|
||||
tf = TfidfVectorizer(analyzer='word',ngram_range=(1, 2),min_df=0.0, stop_words='english')
|
||||
tfidf_matrix = tf.fit_transform(smd['description'])
|
||||
|
||||
# On utilise la Cosine Similarity (similarité cosinus), qui sert à déterminer la similarité de 2 vecteurs
|
||||
# de dimension n en déterminant le cosinus de leur angle. On l'utilise ici avec notre matrice de vecteurs
|
||||
# de pertinence des mots pour avoir les films les plus similaires en fonction des taglines et descriptions.
|
||||
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
|
||||
|
||||
# On récupère les films les plus similaires grâce à la matrice de similarité calculée précédemment
|
||||
smd = smd.reset_index()
|
||||
titles = smd['title']
|
||||
indices = pd.Series(smd.index, index=smd['title'])
|
||||
|
||||
# Méthode pour recommandation en fonction des contents
|
||||
def get_recommendations(title):
|
||||
idx = indices[title]
|
||||
sim_scores = list(enumerate(cosine_sim[idx]))
|
||||
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
|
||||
sim_scores = sim_scores[1:31]
|
||||
movie_indices = [i[0] for i in sim_scores]
|
||||
return titles.iloc[movie_indices]
|
||||
|
||||
#print(get_recommendations('The Dark Knight').head(10))
|
||||
|
||||
|
||||
|
||||
|
||||
# Metadata Based Recommander (genre, keywords, cast, crew)
|
||||
|
||||
|
||||
# On récupère les credits (pour cast et crew) et les keywords
|
||||
credits = pd.read_csv('../Databases/credits.csv')
|
||||
keywords = pd.read_csv('../Databases/keywords.csv')
|
||||
|
||||
# On cast les ids en int
|
||||
keywords['id'] = keywords['id'].astype('int')
|
||||
credits['id'] = credits['id'].astype('int')
|
||||
md['id'] = md['id'].astype('int')
|
||||
|
||||
# On merge les databases avec les metadata des films, pour avoir toutes nos données dans un seul dataframe
|
||||
md = md.merge(credits, on='id')
|
||||
md = md.merge(keywords, on='id')
|
||||
|
||||
# Après avoir merge les tables, on récupère les data qui sont aussi dans links
|
||||
smd = md[md['id'].isin(links_small)]
|
||||
|
||||
# On récupère les données et on les transforme pour avoir ce dont on a besoin ensuite
|
||||
smd['cast'] = smd['cast'].apply(literal_eval)
|
||||
smd['crew'] = smd['crew'].apply(literal_eval)
|
||||
smd['keywords'] = smd['keywords'].apply(literal_eval)
|
||||
smd['cast_size'] = smd['cast'].apply(lambda x: len(x))
|
||||
smd['crew_size'] = smd['crew'].apply(lambda x: len(x))
|
||||
|
||||
# Méthode pour récupérer le réalisateur, puisque c'est la seule donnée qui nous intéresse dans le crew (c'est
|
||||
# la seule qui peut avoir une influence notable sur les préférences des utilisateurs).
|
||||
def get_director(x):
|
||||
for i in x:
|
||||
if i['job'] == 'Director':
|
||||
return i['name']
|
||||
return np.nan
|
||||
|
||||
# On applique la méthode précédente sur nos données pour ne récupérer que les réalisateurs
|
||||
smd['director'] = smd['crew'].apply(get_director)
|
||||
|
||||
# On nettoie les données du cast, et on ne récupère que les 3 acteurs principaux, puisque les autres ne sont pas
|
||||
# forcément assez important pour changer les préférences et avis des utilisateurs.
|
||||
smd['cast'] = smd['cast'].apply(lambda x: [i['name'] for i in x] if isinstance(x, list) else [])
|
||||
smd['cast'] = smd['cast'].apply(lambda x: x[:3] if len(x) >=3 else x)
|
||||
|
||||
# On nettoie les données des keywords pour pouvoir les utiliser
|
||||
smd['keywords'] = smd['keywords'].apply(lambda x: [i['name'] for i in x] if isinstance(x, list) else [])
|
||||
|
||||
# On enlève les espaces entre les noms et on met tout en lowercase pour pouvoir comparer les données
|
||||
smd['cast'] = smd['cast'].apply(lambda x: [str.lower(i.replace(" ", "")) for i in x])
|
||||
|
||||
# On fait pareil pour le réalisateur, et on le compte 3 fois pour qu'il ait autant de poids que les acteurs principaux
|
||||
smd['director'] = smd['director'].astype('str').apply(lambda x: str.lower(x.replace(" ", "")))
|
||||
smd['director'] = smd['director'].apply(lambda x: [x,x, x])
|
||||
|
||||
# On s'occupe des keywords, on calcule le nombre d'utilisation des keywords dans notre dataset pour choisir les plus pertinents
|
||||
s = smd.apply(lambda x: pd.Series(x['keywords']),axis=1).stack().reset_index(level=1, drop=True)
|
||||
s.name = 'keyword'
|
||||
|
||||
# On compte pour chaque value le nombre de fois où elle apparaît
|
||||
s = s.value_counts()
|
||||
# On récupère les keywords qui apparaissent plus d'une fois, puisque les autres ne nous intéressent pas dans le cadre d'une comparaison
|
||||
s = s[s > 1]
|
||||
|
||||
# Cet objet sert à uniformiser les données (ex: chiens et chien équivaudront au même mot)
|
||||
stemmer = SnowballStemmer('english')
|
||||
|
||||
# Méthode pour filtrer les keywords (on récupère ceux qui sont dans s)
|
||||
def filter_keywords(x):
|
||||
words = []
|
||||
for i in x:
|
||||
if i in s:
|
||||
words.append(i)
|
||||
return words
|
||||
|
||||
# On filtre les keywords, on uniformise les données avec le stemmer, et on met tous les keywords en lowercase et sans espace
|
||||
smd['keywords'] = smd['keywords'].apply(filter_keywords)
|
||||
smd['keywords'] = smd['keywords'].apply(lambda x: [stemmer.stem(i) for i in x])
|
||||
smd['keywords'] = smd['keywords'].apply(lambda x: [str.lower(i.replace(" ", "")) for i in x])
|
||||
|
||||
# On mélange toutes les données récupérées pour chaque film
|
||||
smd['soup'] = smd['keywords'] + smd['cast'] + smd['director'] + smd['genres']
|
||||
smd['soup'] = smd['soup'].apply(lambda x: ' '.join(x))
|
||||
|
||||
# CountVectorizer is a text preprocessing technique commonly used in natural language processing (NLP)
|
||||
# tasks for converting a collection of text documents into a numerical representation.
|
||||
# Comme tout à l'heure, on créé une matrice de la fréquence d'apparition dans 'soup' pour pouvoir comparer les films.
|
||||
count = CountVectorizer(analyzer='word',ngram_range=(1, 2),min_df=0.0, stop_words='english')
|
||||
count_matrix = count.fit_transform(smd['soup'])
|
||||
|
||||
# On créé maintenant la matrice de similarité de la précédente en utilisant la similarité cosinus
|
||||
cosine_sim = cosine_similarity(count_matrix, count_matrix)
|
||||
|
||||
# On récupère les titres des films pour pouvoir afficher le résultat
|
||||
smd = smd.reset_index()
|
||||
titles = smd['title']
|
||||
indices = pd.Series(smd.index, index=smd['title'])
|
||||
|
||||
# On réutilise la même méthode, et on attend de nouveaux résultats avec notre nouvelle matrice de similarité
|
||||
#print(get_recommendations('Shrek').head(10))
|
||||
|
||||
|
||||
|
||||
|
||||
# Ratings + Content Recommender
|
||||
|
||||
|
||||
# Dans cette méthode, on utilise à la fois le code pour les recommandations en fonction de la popularité et en fonction du content
|
||||
def improved_recommendations(title):
|
||||
idx = indices[title]
|
||||
sim_scores = list(enumerate(cosine_sim[idx]))
|
||||
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
|
||||
sim_scores = sim_scores[1:26]
|
||||
movie_indices = [i[0] for i in sim_scores]
|
||||
|
||||
movies = smd.iloc[movie_indices][['title', 'vote_count', 'vote_average', 'year']]
|
||||
vote_counts = movies[movies['vote_count'].notnull()]['vote_count'].astype('int')
|
||||
vote_averages = movies[movies['vote_average'].notnull()]['vote_average'].astype('int')
|
||||
C = vote_averages.mean()
|
||||
m = vote_counts.quantile(0.60)
|
||||
qualified = movies[(movies['vote_count'] >= m) & (movies['vote_count'].notnull()) & (movies['vote_average'].notnull())]
|
||||
qualified['vote_count'] = qualified['vote_count'].astype('int')
|
||||
qualified['vote_average'] = qualified['vote_average'].astype('int')
|
||||
qualified['wr'] = qualified.apply(weighted_rating, axis=1)
|
||||
qualified = qualified.sort_values('wr', ascending=False).head(10)
|
||||
return qualified
|
||||
|
||||
# On affiche les films recommandés avec l'algorithme prenant en compte tous les paramètres
|
||||
#print(improved_recommendations('The Dark Knight'))
|
||||
|
||||
|
||||
|
||||
|
||||
# Recommendations for users
|
||||
|
||||
|
||||
# On créé un reader pour parser les ratings
|
||||
reader = Reader()
|
||||
|
||||
# On récupère les ratings
|
||||
ratings = pd.read_csv('../Databases/ratings_small.csv')
|
||||
|
||||
# On utilise cette méthode pour classer les movies en fonction des ratings donnés par les utilisateurs
|
||||
data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)
|
||||
|
||||
# On utilise SVD (Singular Value Decomposition)
|
||||
# Singular value decomposition (SVD) is a method of representing a matrix as a series of linear approximations
|
||||
# that expose the underlying meaning-structure of the matrix. The goal of SVD is to find the optimal set of
|
||||
# factors that best predict the outcome.
|
||||
svd = SVD()
|
||||
|
||||
# On train notre modèle pour qu'il arrive à définir la note qu'un utilisateur pourrait donner à un film en fonction des notes qu'il
|
||||
# a données aux autres films et des notes que les utilisateurs ayant les mêmes goûts que lui ont donné au film en question.
|
||||
trainset, testset = train_test_split(data, test_size=0.25)
|
||||
predictions = svd.fit(trainset).test(testset)
|
||||
|
||||
#print(predictions)
|
||||
|
||||
# Root Mean Square Error
|
||||
accuracy.rmse(predictions)
|
||||
|
||||
# Mean Absolute Error
|
||||
accuracy.mae(predictions)
|
||||
|
||||
# On essaie de predire la note que l'utilisateur 1 va donner au film 302
|
||||
#print(svd.predict(1, 302, 3))
|
||||
|
||||
|
||||
|
||||
|
||||
# Hybrid recommender
|
||||
|
||||
|
||||
# Méthode pour convert les data en int (gestion des data null)
|
||||
def convert_int(x):
|
||||
try:
|
||||
return int(x)
|
||||
except:
|
||||
return np.nan
|
||||
|
||||
# On récupère les links, avec les données qui nous intéressent
|
||||
id_map = pd.read_csv('../Databases/links_small.csv')[['movieId', 'tmdbId']]
|
||||
id_map['tmdbId'] = id_map['tmdbId'].apply(convert_int)
|
||||
id_map.columns = ['movieId', 'id']
|
||||
id_map = id_map.merge(smd[['title', 'id']], on='id').set_index('title')
|
||||
|
||||
indices_map = id_map.set_index('id')
|
||||
|
||||
# Méthode qui utilise plusieurs critères (préférences des utilisateurs et content des films)
|
||||
def hybrid(userId, title):
|
||||
idx = indices[title]
|
||||
tmdbId = id_map.loc[title]['id']
|
||||
movie_id = id_map.loc[title]['movieId']
|
||||
|
||||
sim_scores = list(enumerate(cosine_sim[int(idx)]))
|
||||
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
|
||||
sim_scores = sim_scores[1:26]
|
||||
movie_indices = [i[0] for i in sim_scores]
|
||||
|
||||
movies = smd.iloc[movie_indices][['title', 'vote_count', 'vote_average', 'year', 'id']]
|
||||
movies['est'] = movies['id'].apply(lambda x: svd.predict(userId, indices_map.loc[x]['movieId']).est)
|
||||
movies = movies.sort_values('est', ascending=False)
|
||||
return movies.head(10)
|
||||
|
||||
# On a des recommandations différentes pour des utilisateurs différents, notre algorithme permet donc une recommandation
|
||||
# en fonction du user et de ses préférences
|
||||
# print(hybrid(1, 'Avatar'))
|
||||
# print(hybrid(500, 'Avatar'))
|
||||
|
||||
# L'utilisateur peut rentrer un utilisateur et un titre de film en ligne de commande
|
||||
print("Rentrer 1 et Avatar pour avoir les recommandations pour l'utilisateur 1 et le film Avatar par exemple")
|
||||
print("User Id : ")
|
||||
userId = input()
|
||||
print("Titre du film : ")
|
||||
titreFilm = input()
|
||||
print(hybrid(userId, titreFilm))
|
Loading…
Reference in new issue