Recommandation de musique

On reprend le sujet en indiquant des éléments de correction sous cette forme :

élément de correction...

Dans ce TP, on continue notre exploration de notions et techniques de base pour la science des données. On va notamment manipuler un fichier de données beaucoup plus gros que précédemment. Après avoir exploré ce jeu de données, on s'intéressera à la recommandation de produits.
Tous les TPs précédents doivent impérativement avoir été faits.

Exploration du jeu de données

Introduction

Dans ce TP, nous allons utiliser un jeu de données qui contient tous les morceaux de musique de 1960 à 2009 qui ont été classés au Billboard aux États-Unis, c'est-à-dire les morceaux les plus populaires durant cette période.

Ce fichier de données est disponible à l'url suivante : https://philippe-preux.github.io/ensg/miashs/l3-sd2/datasets/EvolutionPopUSA_MainData.csv.

À faire :

Exploration initiale du jeu de données

Ce jeu de données indiquant les morceaux populaires au fil des années, on peut étudier l'évolution des styles de musique populaire au cours de ces dernières décennies.

Ce jeu de données contient de nombreuses données, chacune décrite par de nombreux attributs. On a :

S'agissant d'un si gros jeu de données, il est vraiment indispensable de passer un peu de temps pour comprendre sa structure et aussi faire en sorte que les champs soient bien typés. C'est le but des questions qui suivent.

À faire :

Amélioration de la structure du jeu de données

Plusieurs choses sont à faire sur ce jeu de données avant de l'utiliser.

À faire :

Sorties au fil du temps

Popularité des genres musicaux au fil du temps

Pour chaque genre musical, il faut compter le nombre de sorties par année et diviser celui-ci par le nombre total (tous genres confondus) de sorties durant l'année. On commence par calculer le nombre de sorties total par année (dans le vecteur vc), puis on calcule le nombre de sorties par genre par année (vc2), on divise ce dernier par le premier et on l'affiche.

# je définis une liste avec le nom des 13 genres
genres = ["northern soul/soul/hip hop/dance",
          "hip hop/rap/gangsta rap/old school",
          "easy listening/country/love song/piano",
          "funk/blues/jazz/soul",
          "rock/pop/new wave",
          "female voice/pop/R'n'B/Motown",
          "country/classic country/folk/rockability",
          "dance/new wave/pop/electronic",
          "classic rock/country/rock/singer-songwriter",
          "love song/slow jams/soul/folk",
          "funk/blues/dance/blues rock",
          "soul/R'n'B/funk/disco",
          "rock/hard rock/alternative/classic-rock"]

vc = music ["year"].value_counts().sort_index()
colors = ["blue", "red", "green", "pink", "yellow", "m", "aquamarine", "fuchsia", "sandybrown", "coral", "teal", "olive", "slateblue"]
for numero in range (13):
    fig, ax = plt.subplots ()
    vc2 = music [music.loc [:, "cluster"] == numero + 1].loc [:, "year"].value_counts()
    ax.plot (range (1960, 2010), vc2 / vc, c = colors [numero], ls = "-", label = genres [numero])
    ax.set_title ("Proportion de sorties par année pour le genre musical " + str (numero + 1) + "\n(" + genres [numero] + ")")
    ax.set_ylim (bottom = 0, top = .3)
    fig.show ()

Popularité des artistes

On compte les occurences des données pour chaque valeur de l'attribut artist_name_clean et on retient les valeurs apparaissant plus de 30 fois.

music.loc [:,"artist_name_clean"].value_counts ().index [music.loc [:,"artist_name_clean"].value_counts () > 30]

qui se comprend en décomposant cette ligne :

Remarque : le résultat est différent si on fait ce décompte sur l'attribut artist_name, signe qu'un même artiste est représenté de différentes manières par cet attribut.
Vous pouvez chercher de quel(s) artiste(s) il s'agit, c'est un bon exercice complémentaire.

music.loc[:,"artist_name_clean"].value_counts() fournit le nombre de morceaux par artiste. Pour répondre à cette question, il faut appliquer value_counts() sur le résultat, soit music.loc[:,"artist_name_clean"].value_counts().value_counts().
Cela nous indique qu'il y a un artiste qui a eu 96 morceaux (ce que l'on a identifié plus tôt). On retrouve que 33 artistes ont au moins 30 titres (ou 28 ont eu plus de 30 titres). etc
Donc, il faut juste mettre le résultat dans l'objet decompte.

fig, ax = plt.subplots ()
ax.plot (decompte.to_numpy ())
ax.set_title ("Répartition du nombre de titres par artiste.")
fig.show ()

L'application de to_numpy() est nécessaire pour que seuls les décomptes soient affichés : comparez le résultat de decompte et de decompte.to_numpy() pour bien comprendre.

fig, ax = plt.subplots ()
ax.loglog(decompte.to_numpy())
ax.set_title ("Répartition du nombre de titres par artiste en échelles logarithmiques.")
fig.show()

Ce qui est remarquable est que ce graphique est presque une droite. Dans ce contexte où on étudie la distribution des réalisations d'une variable aléatoire et où un graphique en échelles logarithmiques donne (presque) une droite, on sait que se cache derrière un type de processus très général, omniprésent dans la nature mais aussi dans les sociétés. Ce type de processus génère très souvent de très petites valeurs (ici 1) et de plus en plus rarement, il génère des valeurs plus grandes. La taille des cratères lunaires est distribuée selon une telle loi (il y en a beaucoup de petits, très peu de très grands), de même que la fréquence des mots dans d'une langue (il y a des mots qu'on utilise beaucoup, d'autres très rarement) ou l'intensité des éruptions volcaniques. On appelle cela une loi de puissance.
Plot log-log du decompte.

Recommandation

La recommandation est un élément essentiel des sites Internet diffusant de la musique, et plus généralement, des sites de commerce électronique. La réalisation d'un système de recommandation qui fonctionne vraiment bien est quelque chose d'assez sophistiqué, chaque entreprise ayant ses secrets de fabrication pour être meilleure, ou originale, par rapport à ses concurrentes. Néanmoins, il existe quelques principes simples sur lesquels ces systèmes s'appuient. Nous allons explorer l'un d'eux sur ce jeu de données, la recommandation par le contenu.

Quelques idées trop simples

On peut commencer par recommander des morceaux en fonction du nombre de l'artiste : si j'écoute Thriller de Michael Jackson, que peut-on me proposer ? Les autres titres de Michael Jackson.

À faire : écrire une fonction qui prend en paramètre le numéro d'un morceau (par exemple 13954) et renvoie une liste composée des numéros de tous les morceaux de cet artiste. Testez-cette fonction.

On peut utiliser la fonction écrite précédemment : music.loc [recherche_artiste ("Michael Jackson"), "track_name"].

Ce n'est pas très intéressant : j'aimerais bien qu'on me propose des titres d'autres artistes.
Je peux recommander tous les morceaux du même genre musicale. Cette fois-ci, je vais avoir plus de 1000 titres et rien de bien spécifique non plus.

À la recherche des attributs utiles pour la recommandation

Pour recommander des items, il faut utiliser des caractéristiques suffisamment précises, mais pas trop. Le nom d'un artiste est trop précis ; le genre musical est à la fois pas assez précis et trop restrictif.

En général, si on apprécie un morceau de musique, c'est à cause de ses caratcéristiques musicales, en particulier des choses comme la tonalité mais aussi d'autres assez complexes comme des suites d'accords.
Dans le jeu de données que nous étudions, les attributs numérotés à partir de 12 contiennent de telles caractéristiques musicales. Ils ont été calculés à partir des fichiers audios de chacun des morceaux. Cet ensemble d'attributs caractérisent assez finement chaque morceau. Aussi, pour recommander des morceaux, on va utiliser ces attributs : si j'apprécie tel morceau, je vais chercher les autres morceaux qui ont à peu près les mêmes caractéristiques musicales.

Similarité entre deux morceaux de musique

La mise en œuvre de l'idée « les autres morceaux qui ont à peu près les mêmes caractéristiques musicales » nécessite de définir une notion de similarité : plus la similarité est grande entre deux morceaux, plus il y a de chance que si on apprécie l'un, on apprécie le second et que donc, si on apprécie l'un, on peut recommander l'autre.
La définition de cette notion de similarité est capitale : si elle est bien définie, la recommandation va fonctionner, sinon elle ne fonctionnera pas.
C'est tout un art que de définir cette similarité : c'est la partie la plus difficile. Chaque morceau possède un vecteur de 259 composantes caractérisant le style musical du morceau. On peut essayer de toutes les utiliser, ou en utiliser seulement un sous-ensemble. C'est impossible a priori de savoir lesquelles de ces composantes donneront les recommandations les plus intéressantes.
Quoiqu'il en soit, un morceau de musique va être représenté par un vecteur contenant un certain nombre de composantes numériques. Pour quantifier la similarité entre deux mroceaux de musique, on va calculer l'angle entre ces deux vecteurs : si cet angle est petit, cela signifie que les deux vecteurs ont des composantes proches. Plus précisément, on calcule le cosinus de cet angle : plus l'angle est petit, plus le cosinus est proche de 1. Par ailleurs, le cosinus de l'angle entre deux vecteurs normés se calcule très facilement et très rapidement : c'est le produit scalaire des deux vecteurs.
Il faut donc que les vecteurs soient normés, c'est-à-dire de norme 1.
On considère les attributs numérotés 11 à 27. Ainsi, chaque mrceau de musique est caractérisé par un vecteur ayant 16 composantes.
À faire : ces vecteurs sont-ils normés ? S'ils ne le sont pas, normez-les. On peut utiliser la fonction numpy.linalg.norm (v) qui renvoie la norme du vecteur v. Pour normer un vecteur v, il suffit donc de diviser chacu de ses éléments par sa norme.
Conseil : tous ces attributs sont numériques. Pour simplifier les manipulations, je vous conseille de mettre ces attributs dans une matrice et de travailler avec cette matrice dans ce qui suit.

Comme conseillé, je mets les attributs dans une matrice dénommée m. C'est vraiment juste pour se simplifier la vie et les notations.

m = np.array (music.iloc [:, 11:27], dtype=float)

Je calcule la norme pour une ligne prise au hasard et je constate que np.linalg.norm (m [2307, ]) est différent de 1.
Pour normer, on va faire une boucle sur chaque ligne et simplement appliqué la formule mathématique :

for ligne in range (music.shape [0]):
    norme = np.linalg.norm (m [ligne, ])
    m [ligne, ] = m [ligne, ] / norme

Pour trouver les morceaux proches du morceau numéro i, on calcule donc la similarité entre ce morceau et tous les autres (on stocke ces similarités dans un vecteur) et on cherche ensuite les morceaux pour lesquels cette similarité est la plus grande. Comme on vient de le dire, la similarité entre les morceaux numéros i et j est définie par la valeur du produit scalaire entre les vecteurs normés représentant les morceaux i et j (les 16 attributs indiqués plus haut, normés).

À faire : calculer la similarité entre la donnée 22 et toutes les autres. Faites un graphique de la distribution de cette similarité.

v = np.zeros (music.shape [0]) # ce vecteur va contenir la similarité entre le morceau 22 et tous les autres
for i in range (music.shape [0]):
    v [i] = np.dot (m [numero,], m [i, ])
# et on affiche le graphique
fig, ax = plt.subplots ()
ax.hist (v, bins = 100, color = couleurs [indice])
ax.set_title ("Répartition de la similarité des titres à la donnée 22")
fig.show ()

Distribution de la similarité entre la donnée 23 et toutes les autres

Faites de même pour les données 146, 5383 et 13954. Vous devez obtenir des graphiques comme ceux-ci :
Distribution de la similarité entre la donnée 146 et toutes les autres Distribution de la similarité entre la donnée 5383 et toutes les autres Distribution de la similarité entre la donnée 13954 et toutes les autres
Interprêtez et discutez ces graphiques.

Recommandation

Considérons le morceau numéro i. On calcule la similarité entre ce morceau et chacun des autres. Ces similarités sont stockés dans un vecteur. En cherchant les 10 morceaux pour lesquels la similarité est la plus grande, on trouve les 10 morceaux à recommander.
Pour faire cela, il faut déterminer les 10 morceaux pour lesquels la similarité est la plus grande. On utilisera la fonction argsort ().

À faire : déterminer les 10 morceaux les plus similaires à des morceaux que vous connaissez (pour pouvoir juger de la pertinence du résultat). Êtes-vous satisfait du résultat ? Si oui, tant mieux. Si non, il ne vous reste plus qu'à améliorer cette procédure (pour concevoir cet énoncé de TP, j'ai cherché des attributs qui ont l'air de donner des résultats assez satisfaisants ; aussi, je n'ai pas « la » bonne réponse à la question).

Je reprends la donnée 22 utilisée plus haut.

v = np.zeros (music.shape [0])
for i in range (music.shape [0]):
    v [i] = np.dot (m [22,], m [i, ])
argsimilarite_aux_voisins = np.argsort (v)
print (music.artist_name [argsimilarite_aux_voisins [music.shape[0]-10:music.shape[0]]])

Pour aller plus loin

Je propose quelques activités libres à partir de ce que nous avons fait. Si vous obtenez des résultats intéressants, n'hésitez pas à me le dire.

  1. On peut s'intéresser à la recommandation de morceaux de musique en fonction d'un ensemble de morceaux (ci-dessus, on fait une recommandation en fonction d'un seul morceau). Concevoir et implanter une fonction qui prend en paramètre une liste de morceaux et renvoie une liste de recommandations.
  2. On peut utiliser d'autres attributs que les 16 que nous avons utilisés ci-dessus. J'ai essayé d'utiliser tous les attributs à partir du douxième mais les résultats obtenus en utilisant seulement les 16 attributs me semblaient plus pertinents. N'hésitez pas à chercher si d'autres attributs ne donneraient pas des résultats plus intéressants.
    Il est bon de savoir que les 16 attributs numérotés 27 à 41 (leur nom commence par PC) résulent d'une ACP des 16 attributs que nous avons utilisés : ces deux jeux de 16 attributs contiennent donc la même information et il ne faut pas utiliser les deux.

Remarque finale

Les systèmes de recommandation utilisent des informations du type : les personnes écoutant tel morceau écoutent aussi tels autres morceaux. Nous ne disposons pas de ce type d'information ici, d'où des performances plutôt décevantes. Par expérience, on sait que ces informations sont bien plus utiles pour réaliser de bonnes recommandation que les informations concernant les items eux-mêmes.

Référence

Ce TP est basé sur les données issues de l'article :
[1] Mauch M., MacCallum RM, Levy M, Leroi AM. 2015 The evolution of popular music USA 1960-2010. R.Soc. open sci. 2:150081.