You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
15 KiB
347 lines
15 KiB
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)) |