On reprend le sujet en indiquant des éléments de correction sous cette forme :
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.
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 :
music = pd.read_csv ("https://philippe-preux.github.io/ensg/miashs/l3-sd2/datasets/EvolutionPopUSA_MainData.csv")
music.shape [0]
music.shape [1]
list (music)
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 :
music ["artist_name_clean"] = music ["artist_name_clean"].astype("category")
music.loc [:,"first_entry"] = pd.to_datetime (music.loc [:,"first_entry"])
music.loc [:,"artist_name_clean"].unique()
def recherche_artiste (name): liste = [] for i in range(music.shape[0]): if music.at [i,"artist_name"].find (name) != -1: liste.append (i) return (liste) recherche_artiste ("Swift") >>> [8589, 8590, 8591, 8592, 8593, 8890, 10049, 10050, 10051, 10052, 11249, 11351, 11352, 11396, 11397, 11398, 11939, 13919]Attention, dans certains cas, rechercher seulement le nom d'un artiste produit des résultats correspondant à plusieurs artistes portant le même nom. Par exemple, regardez de près le résultat de recherche_artiste ("Jackson") ; le champ artist_name_clean est là pour résoudre ce problème, à moins d'indiquer le prénom et le nom (par exemple, recherche_artiste ("Janet Jackson")).
Plusieurs choses sont à faire sur ce jeu de données avant de l'utiliser.
À faire :
import datetime as dt music ["month"] = music.loc [:,"first_entry"].dt.month
On peut faire cela avec une boucle :
for i in range (music.shape [0]): music.loc [i, "quarter"] = int (music.loc [i, "quarter"].split()[1][1])
Et si on connait, on écrira plutôt en une seule ligne :
music.loc [:, "quarter"] = music.loc [:, "quarter"].apply (lambda x:int(x.split()[1][1]))
en utilisant une fonction anonyme.
fig, ax = plt.subplots () ax.set_xlabel ("Année") ax.set_ylabel ("Nombre de sorties") ax.plot (music ["year"].value_counts().sort_index(), marker = 'o') ax.set_title ("Nombre de nouveaux morceaux par année.") fig.show()
On voit qu'il y a une diminution du nombre de sorties au fil du temps de 1970 à 2000. Ce nombre a augmenté durant la dernière décennie pour laquelle on dispose de données.
fig, ax = plt.subplots () ax.scatter (range (1, 13), music ["month"].value_counts()) ax.set_title ("Nombre de nouveaux morceaux par mois") fig.show ()
Le graphique est le suivant :
On voit que globalement, le nombre de sorties diminue au fil de l'année.
Je commence par calculer le nombre de sorties pour chaque mois de chaque année. Puis, je fais ce graphique pour 5 années prises au hasard : 1966, 1973, 1985, 1994, 2008.
les_années = range (1960,2010) nbSortiesParAnParMois = np.zeros ([len (les_années), 12]) for y in les_années: v = music.loc [music.loc [:, "year"] == y, :].month.value_counts() for m in range (12): nbSortiesParAnParMois [y - 1960, m] = v [m + 1] années = [1966, 1973, 1985, 1994, 2008] fig, ax = plt.subplots (5,1) for i in range (len (années)): ax[i].plot (nbSortiesParAnParMois [années [i]-1960, :], marker = 'o') ax[i].set_xticklabels ("") ax[i].set_ylabel (str (années [i])) ax[i].set_ylim (bottom = 10, top = 65) ax[0].set_title ("Répartition par mois du nombre de sorties de morceaux\npour différentes années.") ax[4].set_xticks ([0,2,4,6,8,10], ["jan", "mars", "mai", "juil", "sep", "nov"]) fig.show()
ce qui donne :
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 ()
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.
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.
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.
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.
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 ()
Faites de même pour les données 146, 5383 et 13954. Vous devez obtenir des graphiques comme ceux-ci :
Interprêtez et discutez ces graphiques.
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]]])
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.
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.
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.