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))