Recommandation sous R: Movie Lens avec Recommenderlab

Dans cet article, nous allons mettre en œuvre un système de recommandation de films sous R grâce à Recommenderlab.

Nous utiliserons les données de movie lens https://grouplens.org/datasets/movielens/1m/.

Elles consistent en 3 tables:

  • une table movie contenant une liste de 3883 films et leurs genres
  • une table user contenant des informations (âge, sexe, travail, code postal) de 6040 utilisateurs
  • une table ratings contenant les notes des utilisateurs concernant certains films: ils n’ont pas noté les 3883 films !

L’objectif ici est donc de proposer des recommandations de films à chaque utilisateur et d’estimer la note de tous les films, même ceux qu’ils n’ont pas notés.

Retrouvez l’ensemble des fichiers sur mon github.

Chargement des données

Dans un premier temps, chargeons les bibliothèques nécessaires:


library(ggplot2)
library(reshape2)
library(stringr)
library(recommenderlab) 

Chargeons ensuite les trois tables:


ratings=read.csv("ratings_short.csv",sep=",")
users=read.csv("users.csv",sep=",")
movies=read.csv("movies.csv",sep=",")

colnames(ratings)=c("UserID","MovieID","Rating","Timestamp")
colnames(users)=c("UserID","Gender","Age","Occupation","Zipcode")
colnames(movies)=c("MovieID","Title","Genres")

On en profite pour donner des noms de colonnes plus parlant.

Attention: Les fichiers originaux ne sont pas des csv classiques, le séparateur est « :: ». J’ai donc préalablement convertis ces fichiers en ‘,’ .

Pour des considérations de performances, je n’utilise ici que les 500 000 premières lignes de ratings. Cela affectera certainement les scores finaux, mais il suffit de charger tout le fichier une fois que la méthode sera bien en place.

Observons maintenant les données:

Genre (films)

View(table(movies$Genres))

dist_movies_genres

Genre (m/f)


round(prop.table(table(users$Gender))*100) # proportion
#F M
# 28 72
ggplot(users,aes(x=Gender))+geom_bar(fill="#FF9999")+scale_fill_brewer(palette = "Pastel1")+theme(axis.text.x =element_text(,hjust = 1,size=10))

hist_users_gender

Score


round(prop.table(table(ratings$Rating))*100) # proportion
 #1 2 3 4 5
 #6 11 26 35 22
ggplot(ratings,aes(x=Rating))+geom_bar(fill="#FF9999")+scale_fill_brewer(palette = "Pastel1")+theme(axis.text.x =element_text(,hjust = 1,size=10))

hist_ratings_rating

Les données ne sont pas équilibrées, ce qui est attendu dans ce genre de bases qui reposent sur le volontariat.

Le déséquilibre n’est pas si important pour la recommandation car il repose sur les seules données contenues dans la table ratings. La performance du système est plutôt liée à la quantité totale de films effectivement notés et ne dépend pas des données disponible dans users ou movies (voir ci-dessous).

Matrice utilisateurs x films

Le système nécessite la création d’une matrice disposant les utilisateurs en ligne et les films en colonnes. Chaque case correspond alors à la note d’un utilisateur pour un film donné:

matrice.PNG

Matrice utilisateur x film

Par exemple, l’utilisateur 8 (u8) à donnée 4 au film 1 (movie1), 3 au film 4, mais n’a pas noté les films 2 et 3 etc….

La création de cette matrice en R est très aisée grâce à la fonction acast() de reshape2:


ratings$Timestamp=NULL #supprimer la colonne Timestamp

affinity=acast(ratings,UserID ~ MovieID ) # creation de la matrice UserID X MovieID
 ### ne pas remplacer NA par 0 !!!!

dim(affinity)
#[1] 3069 3618
rownames(affinity)=c(paste("u", 1:dim(affinity)[1], sep="")) #generer des noms en u****
colnames(affinity)=c(paste("movie", 1:dim(affinity)[2], sep="")) #generer des noms en movie****

D’abord, nous retirons la colonne timestamp.

Puis nous appliquons acast() pour produire la matrice affinity. Il suffit de donner en argument les deux colonnes de ratings à croiser.

Enfin, nous générons des noms de colonnes pour faciliter les opérations ultérieures. Nous obtenons la matrice illustrée plus haut.

Attention, il ne faut pas remplacer les NA de la matrice par 0: le recommender s’attend à une matrice creuse (sparse matrix).

Recommenderlab nécessite une étape supplémentaire de codage de la matrice as « realRatingMatrix ». Nous pouvons ensuite normaliser et éventuellement afficher le résultat avec getRatingMatrix():


affinity.matrix= as(affinity,"realRatingMatrix")
affinity.matrix = normalize(affinity.matrix) ## NORMALISER
getRatingMatrix(affinity.matrix) ## display

Recommendations

Nous sommes prêt à calculer les recommandations, la syntaxe est très simple:


Rec.model=Recommender(affinity.matrix, method = "UBCF")

Différents méthodes sont disponibles, ici nous testons UBCF (Recommender based on user-based collaborative filtering) i.e. filtrage collaboratif: les utilisateurs sont regroupés par goût commun avec l’hypothèse que chaque groupe aura une opinion proche sur chaque film. Ce filtre se distingue du IBCF (Recommender based on item-based collaborative filtering) filtrage par article où l’on recherche les similarités entre les films pour proposer ceux ressemblant le plus au goût de l’utilisateur. Il existe d’autres filtres comme filtre par popularité, par notes élevées, par moindre carrés, etc….

Prédictions

Nous pouvons à présent calculer les recommandations pour chaque utilisateur, grâce à la fonction predict():


user1 = predict(Rec.model, affinity.matrix["u1",], n=5) # top5 film recommande pour l'user1(u1)
as(user1, "list")
# $u1
# [1] "movie283" "movie3352" "movie2339" "movie446" "movie3515"

Ici, nous calculons les n=5 films les plus recommandés pour u1. Il suffit de remplacer u1 (ligne 1) pour voir d’autres utilisateurs.

On ne peut lire user1 directement, il faut obligatoirement utilisé as(user1, "list").

De même, si on souhaite voir les notes de tous les films, même ceux que u1 n’a pas noté, on utilise l’argument type="ratings" dans predict():


user1.affinity = predict(Rec.model, affinity.matrix["u1",], type="ratings" ) #rating de user1(u1) pour TOUS les films meme ceux qu'il n'a pas note
as(user1.affinity,"list")
# $u1
 # movie2 movie3 movie4 movie5 movie6 movie7 movie8 movie9 movie10 movie11
 # 3.713178 3.938884 4.190476 3.146465 3.930974 4.322581 3.884892 3.712320 4.114713 3.277372
 # movie12 movie13 movie14 movie15 movie16 movie17 movie18 movie19 movie20 movie21
 # 3.826087 3.388889 3.320000 3.288838 3.074466 4.075829 3.649180 3.572549 4.083333 2.909091
 # movie22 movie23 movie24 movie25 movie26 movie27 movie28 movie29 movie30 movie31
 # 3.067340 3.312999 3.975196 3.741176 2.960000 4.171429 3.757009 3.618762 3.488372 3.731092

Tout le travail est donc accompli. Il n’y a plus qu’à faire un peu de mise en forme si on souhaite une fiche détaillée pour u1:

#fiche complete pour user1
user1_df=as.data.frame(as(user1.affinity,"list"))
user1_df$movie=rownames(user1_df) #les index de lignes correspondent aux films : on les places dans une nouvelles colonnes 'movie'
rownames(user1_df)=NULL # on efface les index de lignes
user1_df$movie=str_split_fixed(user1_df$movie, "movie", 2)[,2] # on decoupe les chaines 'movie283' en 'movie' et '283', on ne garde que les numeros
colnames(user1_df)=c("rating","MovieID") 

user1_df=merge(user1_df, movies, by="MovieID") # on fusionne user1_df avec movies pour obtenir la fiche des films pour user1

head(user1_df[order(-user1_df$rating),] )

# MovieID rating Title Genres
# 1949 283 4.962963 New Jersey Drive (1995) Crime|Drama
# 2525 3352 4.962963 Brown's Requiem (1998) Drama
# 1412 2339 4.956522 I'll Be Home For Christmas (1998) Comedy|Romance
# 2706 3515 4.843137 Me Myself I (2000) Comedy
# 2914 446 4.843137 Farewell My Concubine (1993) Drama|Romance
# 2707 3516 4.837838 Bell, Book and Candle (1958) Comedy|Romance
u1_recom

Les top films recommandés pour l’utilisateur u1

Prédictions personnalisées

Pour obtenir vos propre recommandations, il suffirait d’introduire vos notes dans ratings_short.csv en respectant les champs « UserID », »MovieID », »Rating » et « Timestamp », puis de relancer tout le processus en sélectionnant votre id au moment de calculer les prédictions.

Evaluation

Recommenderlab possède des outils pour évaluer la pertinence des modèles:


e = evaluationScheme(affinity.matrix, method="split", train=0.7,given=3)
# creation d'un modele de recommendation type ubcf
Rec.ubcf = Recommender(getData(e, "train"), "UBCF")
# creation d'un modele de recommendation type ibcf pour comparer
Rec.ibcf = Recommender(getData(e, "train"), "IBCF")
# predictions sur test (UBCF)
p.ubcf = predict(Rec.ubcf, getData(e, "known"), type="ratings")
# predictions sur test (IBCF)
p.ibcf = predict(Rec.ibcf, getData(e, "known"), type="ratings")
# Calcul des erreurs pour chaque methodes
error.ubcf = calcPredictionAccuracy(p.ubcf, getData(e, "unknown"))
error.ibcf =calcPredictionAccuracy(p.ibcf, getData(e, "unknown"))
error = rbind(error.ubcf,error.ibcf)
rownames(error) = c("UBCF","IBCF")
error
#       RMSE MSE MAE # lower better
# UBCF 1.160121 1.345882 0.9131636
# IBCF 1.665191 2.772861 1.2743363

Nous comparons ici directement les deux méthodes IBCF et UBCF:

  1. Partitionnement en deux jeux train et known pour la validation croisée
  2. Apprentissage pour les deux méthodes
  3. Prédictions pour les deux méthodes
  4. Calcul des erreurs.

Nous observons un très léger avantage pour la méthode UBCF.

Code complet

github


library(ggplot2)
library(reshape2)
library(stringr)
library(recommenderlab)	

#https://cran.r-project.org/web/packages/recommenderlab/vignettes/recommenderlab.pdf

ratings=read.csv("ratings_short.csv",sep=",")
users=read.csv("users.csv",sep=",")
movies=read.csv("movies.csv",sep=",")
#ratings=ratings[1:500000,]#eco memoire si besoin

colnames(ratings)=c("UserID","MovieID","Rating","Timestamp")
colnames(users)=c("UserID","Gender","Age","Occupation","Zipcode")
colnames(movies)=c("MovieID","Title","Genres")

round(prop.table(table(movies$Genres))*100)  # proportion

round(prop.table(table(users$Gender))*100)  # proportion
#F  M
# 28 72
ggplot(users,aes(x=Gender))+geom_bar(fill="#FF9999")+scale_fill_brewer(palette = "Pastel1")+theme(axis.text.x =element_text(,hjust = 1,size=10))

round(prop.table(table(ratings$Rating))*100)  # proportion
 #1  2  3  4  5
 #6 11 26 35 22
ggplot(ratings,aes(x=Rating))+geom_bar(fill="#FF9999")+scale_fill_brewer(palette = "Pastel1")+theme(axis.text.x =element_text(,hjust = 1,size=10))

################preparation
ratings$Timestamp=NULL #enlever Timestamp

affinity=acast(ratings,UserID ~ MovieID ) # creation de la matrice UserID X MovieID
 ### ne pas remplacer NA par 0 !!!!

dim(affinity)
#[1] 3069 3618
rownames(affinity)=c(paste("u", 1:dim(affinity)[1], sep="")) #generer des noms en u****
colnames(affinity)=c(paste("movie", 1:dim(affinity)[2], sep="")) #generer des noms en movie****

affinity.matrix= as(affinity,"realRatingMatrix")
affinity.matrix = normalize(affinity.matrix) ## NORMALISER
getRatingMatrix(affinity.matrix) ## display

Rec.model=Recommender(affinity.matrix, method = "UBCF")
#Recommender based on user-based collaborative filtering

######predictions
user1= predict(Rec.model, affinity.matrix["u1",], n=5) # top5 film recommande pour l'user1(u1)
as(user1, "list")
# $u1
# [1] "movie283"  "movie3352" "movie2339" "movie446"  "movie3515"

user1.affinity = predict(Rec.model, affinity.matrix["u1",],type="ratings" ) #rating de user1(u1) pour TOUS les films meme ceux qu'il n'a pas note
as(user1.affinity,"list")
# $u1
   # movie2    movie3    movie4    movie5    movie6    movie7    movie8    movie9   movie10   movie11
 # 3.713178  3.938884  4.190476  3.146465  3.930974  4.322581  3.884892  3.712320  4.114713  3.277372
  # movie12   movie13   movie14   movie15   movie16   movie17   movie18   movie19   movie20   movie21
 # 3.826087  3.388889  3.320000  3.288838  3.074466  4.075829  3.649180  3.572549  4.083333  2.909091
  # movie22   movie23   movie24   movie25   movie26   movie27   movie28   movie29   movie30   movie31
 # 3.067340  3.312999  3.975196  3.741176  2.960000  4.171429  3.757009  3.618762  3.488372  3.731092

##### fiche complete pour user1
user1_df=as.data.frame(as(user1.affinity,"list"))
user1_df$movie=rownames(user1_df) #les index de lignes correspondent aux films -> on les places dans une nouvelles colonnes 'movie'
rownames(user1_df)=NULL # on efface les indices de lignes
user1_df$movie=str_split_fixed(user1_df$movie, "movie", 2)[,2] # on decoupe les chaines 'movie283' en 'movie' et '283', on ne garde que les numeros
colnames(user1_df)=c("rating","MovieID") 

user1_df=merge(user1_df, movies, by="MovieID")  # on fusionne user1_df avec movies pour obtenir la fiche des films pour user1
head(user1_df[order(-user1_df$rating),]  )
     # MovieID   rating                             Title         Genres
# 1949     283 4.962963           New Jersey Drive (1995)    Crime|Drama
# 2525    3352 4.962963            Brown's Requiem (1998)          Drama
# 1412    2339 4.956522 I'll Be Home For Christmas (1998) Comedy|Romance
# 2706    3515 4.843137                Me Myself I (2000)         Comedy
# 2914     446 4.843137      Farewell My Concubine (1993)  Drama|Romance
# 2707    3516 4.837838      Bell, Book and Candle (1958) Comedy|Romance  

############### evaluation

e = evaluationScheme(affinity.matrix, method="split", train=0.7,given=3)
# creation d'un modele de recommendation type ubcf
Rec.ubcf = Recommender(getData(e, "train"), "UBCF")
# creation d'un modele de recommendation type ibcf pour comparer
Rec.ibcf = Recommender(getData(e, "train"), "IBCF")
# predictions sur test (UBCF)
p.ubcf = predict(Rec.ubcf, getData(e, "known"), type="ratings")
# predictions sur test (IBCF)
p.ibcf = predict(Rec.ibcf, getData(e, "known"), type="ratings")
# Calcul des erreurs pour chaque methodes
error.ubcf=calcPredictionAccuracy(p.ubcf, getData(e, "unknown"))
error.ibcf=calcPredictionAccuracy(p.ibcf, getData(e, "unknown"))
error = rbind(error.ubcf,error.ibcf)
rownames(error) = c("UBCF","IBCF")
error
#       RMSE      MSE       MAE   # lower better
# UBCF 1.160121 1.345882 0.9131636
# IBCF 1.665191 2.772861 1.2743363

References

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion /  Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion /  Changer )

w

Connexion à %s