Accéder au contenu principal

Analyse de sentiment « tidy » en R

Apprenez à réaliser une analyse de sentiment « tidy » en R sur les chansons de Prince : évolution du sentiment dans le temps, au niveau des morceaux, impact des bigrammes, et bien plus encore !
Actualisé 22 sept. 2026  · 15 min lire

Explorez l'IA

ChatGPTClaudePerplexity

Embarquez pour un voyage sentimental à travers la vie et l’époque de Prince, The Artist, dans la partie Deux-A d’une série de trois tutoriels, en utilisant l’analyse de sentiment avec R pour éclairer la carrière de l’Artiste et son influence sociétale. Les trois tutoriels couvrent :

Pour aller plus loin sur l’analyse de sentiment, consultez notre cours Sentiment Analysis in R : The Tidy Way.

Saviez-vous que Prince a « prédit » le 11 septembre sur scène, trois ans avant ? Il aurait aussi prédit sa propre mort. Possible ? Ne serait-il pas intéressant d’examiner ses paroles et de vous forger votre propre opinion étayée ? Le 21 avril 1985, il enregistre "Sometimes It Snows In April". Il meurt exactement 31 ans plus tard, le 21 avril 2016. Observez le choix des mots dans l’analyse de sentiment de ce titre :

Sometimes It Snows In April
Analyse de sentiment de "Sometimes It Snows In April"

Voilà qui donne un nouveau sens à l’analytique prédictive !

Structure

Chaque bloc de code est suivi d’un commentaire d’interprétation, souvent subjectif. N’hésitez pas à passer d’une section à l’autre et à formuler vos propres hypothèses si vous préférez. Je vous recommande de lire l’introduction ci-dessous, mais si vous souhaitez plonger directement dans le code, cliquez ici. Si vous maîtrisez déjà la notion de lexiques, vous pouvez aller à l’essentiel en rejoignant directement la section d’analyse détaillée.

Introduction

Trio : synthèse en trois parties

Je viens de recueillir deux chatons et un chiot, alors je pense par trois désormais. Et puis, la règle de trois en écriture veut que les éléments présentés par trois soient plus percutants, mémorables ou efficaces que les autres – d’où ce découpage en trois volets. Dans la Partie un, vous avez découvert la fouille de texte et l’analyse exploratoire à partir d’un jeu de données contenant des centaines de paroles de chansons du légendaire Prince. Dans la Partie deux, vous vous concentrez sur l’analyse de sentiment (Deux-A) et la modélisation de sujets (Deux-B) en NLP. La Partie trois conclura avec des tâches d’analytique prédictive via des techniques de machine learning pour répondre à des questions comme : peut-on déterminer la décennie de sortie d’une chanson ? Peut-on, plus intéressant encore, prédire qu’un titre entrera au Billboard à partir de ses paroles ? Tout au long de ces tutoriels, vous utiliserez différents algorithmes de machine learning – chacun étant mis en évidence en rouge dans le graphique ci-dessous.

Techniques de machine learning
Adapté du schéma présenté ici. Notez que les différentes facettes de la modélisation et des techniques de machine learning ne rentrent pas forcément dans une seule case comme ci-dessus. L’idée est de vous donner un aperçu de ce qui vous attend.

Couper en deux, c’est dur à faire

Oui, j’avais promis trois parties, mais j’ai décidé de scinder la Partie deux en Deux-A (analyse de sentiment) et Deux-B (modélisation de sujets – à venir), ce qui donne quatre tutoriels distincts. Il existe de nombreux excellents articles et cours sur la démarche d’analyse de sentiment ; l’objectif de la Partie Deux-A est donc d’extraire des enseignements de l’analyse – pas seulement d’écrire du code. Nous utiliserons les paroles de Prince comme exemple, mais vous pourrez appliquer ces étapes à votre artiste préféré. J’espère que ce parcours complet attisera votre curiosité sur les possibilités de l’analyse de paroles tout en vous initiant au traitement automatique du langage (NLP) et aux techniques de machine learning.

Prérequis

La Partie Deux-A suppose une compréhension de base des données « tidy » – en particulier des packages comme dplyr pour la transformation des données, ggplot2 pour les visualisations, et l’opérateur pipe %>% (issu initialement de magrittr). Vous verrez que plusieurs étapes sont enchaînées avec %>% pour garder un code compact et réutilisable.

Comme ce tutoriel est aussi une étude de cas, il regorge de visualisations au-delà des graphiques habituels. Autre prérequis : savoir installer de nouveaux packages dans votre environnement. Pour vous laisser vous concentrer sur les insights, j’ai inclus du code conçu pour être réutilisé sur vos propres données, mais une explication exhaustive demandera des recherches complémentaires de votre part. Des liens utiles sont fournis pour chaque package, afin de vous exposer à de nouveaux outils tout en privilégiant leur mise en pratique.

Conseil : lisez d’abord la Partie un pour poser les bases de la série et découvrir le vocabulaire denses et varié des paroles de Prince.

Vue d’ensemble de l’analyse de sentiment

  • Méthodes : l’analyse de sentiment est une forme de fouille de texte visant à déterminer l’opinion et la subjectivité d’un contenu. Appliquée aux paroles, elle peut refléter non seulement l’attitude de l’artiste, mais aussi révéler des influences culturelles plus larges. Plusieurs approches existent : apprentissage supervisé sur un jeu de données annoté, création de vos propres classifieurs par règles, ou utilisation de dictionnaires lexicaux prédéfinis (lexiques). Dans ce tutoriel, vous utiliserez une approche fondée sur les lexiques ; je vous encourage toutefois à explorer les autres méthodes et leurs compromis.

  • Niveaux : comme pour les méthodes, il existe différents niveaux d’analyse selon le texte : document, phrase et mot. Pour des paroles, le « document » peut correspondre au sentiment par décennie, par année, par niveau de classement, ou par chanson. Le niveau phrase est rarement pertinent, la ponctuation pouvant nuire aux rimes et aux schémas. L’analyse au niveau mot révèle des détails utiles et sert de base à des techniques plus avancées en modélisation de sujets.

Préparez vos questions !

Tout projet data doit commencer par des questions à explorer. Gardez celles-ci à l’esprit au fil du tutoriel : peut-on écrire un programme pour déterminer l’humeur exprimée dans des paroles ? Les lexiques prédéfinis suffisent-ils ? Quel niveau de préparation des données est nécessaire ? Pouvez-vous relier vos résultats à des événements réels ? Le sentiment évolue-t-il dans le temps ? Les hits sont-ils plus positifs ou plus négatifs que les morceaux non classés ? Quels mots ressortent l’année où l’on dit que Prince a prédit le 11 septembre ? A-t-il prédit sa propre mort ?

Conseil : l’analyse de paroles est très différente, et souvent plus complexe, que celle de discours ou de livres ; il est difficile d’en extraire des insights fiables, alors soyez prudents avec vos hypothèses. Je poserai plus de questions que je n’apporterai de réponses ; préparez-vous à sortir du cadre classique !

Préparation

Bibliothèques et fonctions

Pour commencer l’analyse des paroles de Prince, chargez les bibliothèques ci-dessous. La liste peut paraître impressionnante, mais la plupart servent aux graphiques. Vu la fréquence des visuels, mieux vaut définir un nuancier standard pour garantir la cohérence. J’ai créé une liste avec des codes couleur ANSI. La fonction theme() de ggplot2 permet de personnaliser chaque graphique ; vous allez donc créer votre propre fonction, theme_lyrics(), pour ajuster les paramètres par défaut. Le package knitr est un moteur de génération de rapports dynamiques avec R. Utilisez-le avec kableExtra et formattable pour produire de belles tables colorées. Créez aussi votre fonction my_kable_styling() afin d’uniformiser le rendu de ces bibliothèques. Les autres packages seront mentionnés au fil de leur utilisation.

library(dplyr) #Data manipulation (also included in the tidyverse package)
library(tidytext) #Text mining
library(tidyr) #Spread, separate, unite, text mining (also included in the tidyverse package)
library(widyr) #Use for pairwise correlation

#Visualizations!
library(ggplot2) #Visualizations (also included in the tidyverse package)
library(ggrepel) #`geom_label_repel`
library(gridExtra) #`grid.arrange()` for multi-graphs
library(knitr) #Create nicely formatted output tables
library(kableExtra) #Create nicely formatted output tables
library(formattable) #For the color_tile function
library(circlize) #Visualizations - chord diagram
library(memery) #Memes - images with plots
library(magick) #Memes - images with plots (image_read)
library(yarrr)  #Pirate plot
library(radarchart) #Visualizations
library(igraph) #ngram network diagrams
library(ggraph) #ngram network diagrams

#Define some colors to use throughout
my_colors <- c("#E69F00", "#56B4E9", "#009E73", "#CC79A7", "#D55E00", "#D65E00")

#Customize ggplot2's default theme settings
#This tutorial doesn't actually pass any parameters, but you may use it again in future tutorials so it's nice to have the options
theme_lyrics <- function(aticks = element_blank(),
                         pgminor = element_blank(),
                         lt = element_blank(),
                         lp = "none")
{
  theme(plot.title = element_text(hjust = 0.5), #Center the title
        axis.ticks = aticks, #Set axis ticks to on or off
        panel.grid.minor = pgminor, #Turn the minor grid lines on or off
        legend.title = lt, #Turn the legend title on or off
        legend.position = lp) #Turn the legend on or off
}

#Customize the text tables for consistency using HTML formatting
my_kable_styling <- function(dat, caption) {
  kable(dat, "html", escape = FALSE, caption = caption) %>%
  kable_styling(bootstrap_options = c("striped", "condensed", "bordered"),
                full_width = FALSE)
}

Données « Sushi »

Comme toujours, commencez par lire les données brutes dans un data frame. Dans la Partie un, vous avez conditionné le jeu de données d’origine : expansion des contractions, suppression des séquences d’échappement, passage en minuscules. Vous avez sauvegardé ce jeu nettoyé en CSV pour ce tutoriel. Utilisez read.csv() pour créer prince_data ; gardez à l’esprit que vous voulez des chaînes de caractères pour les paroles : définissez stringsAsFactors = FALSE. Comme vous n’avez pas besoin d’index de ligne, mettez row.names = 1.

Remarque : vous pourriez éviter ces paramètres en lisant un data frame moderne appelé tibble via read_csv(), mais pour rester cohérent avec la Partie un, nous utilisons read.csv().

prince_data <- read.csv('prince_new.csv', stringsAsFactors = FALSE, row.names = 1)

Jetez un coup d’œil rapide aux données :

 glimpse(prince_data) #Transposed version of `print()`
## Observations: 824
## Variables: 10
## $ lyrics      <chr> "all 7 and we will watch them fall they stand in t...
## $ song        <chr> "7", "319", "1999", "2020", "3121", "7779311", "u"...
## $ year        <int> 1992, NA, 1982, NA, 2006, NA, NA, NA, NA, NA, NA, ...
## $ album       <chr> "Symbol", NA, "1999", "Other Songs", "3121", NA, N...
## $ peak        <int> 3, NA, 2, NA, 1, NA, NA, NA, NA, NA, NA, NA, NA, N...
## $ us_pop      <chr> "7", NA, "12", NA, "1", NA, NA, NA, NA, NA, NA, NA...
## $ us_rnb      <chr> "61", NA, "4", NA, "1", NA, NA, NA, NA, NA, NA, NA...
## $ decade      <chr> "1990s", NA, "1980s", NA, "2000s", NA, NA, NA, NA,...
## $ chart_level <chr> "Top 10", "Uncharted", "Top 10", "Uncharted", "Top...
## $ charted     <chr> "Charted", "Uncharted", "Charted", "Uncharted", "C...

Vous voyez que prince_data contient 824 chansons et 10 colonnes. Autrement dit, un enregistrement est une chanson, littéralement.

Propre et net : prince_tidy

Voici quelques dernières étapes de préparation des données :

  • Supprimer des mots indésirables (liste manuelle de mots non pertinents)
  • Supprimer les stop words (mots très fréquents comme « and », « the », « a », « of », etc.)
  • Supprimer les mots de moins de trois caractères (souvent utilisés pour l’effet phonétique)
  • Découper les paroles en mots individuels

Pour passer à un format tidy, utilisez unnest_tokens() de tidytext pour créer prince_tidy, qui éclate les paroles en mots avec un mot par ligne. Vous pourrez ensuite employer anti_join() et filter() de dplyr pour finaliser le nettoyage. (Voir la Partie un pour l’explication détaillée.)

#Created in the first tutorial
undesirable_words <- c("prince", "chorus", "repeat", "lyrics",
                       "theres", "bridge", "fe0f", "yeah", "baby",
                       "alright", "wanna", "gonna", "chorus", "verse",
                       "whoa", "gotta", "make", "miscellaneous", "2",
                       "4", "ooh", "uurh", "pheromone", "poompoom", "3121",
                       "matic", " ai ", " ca ", " la ", "hey", " na ",
                       " da ", " uh ", " tin ", "  ll", "transcription",
                       "repeats", "la", "da", "uh", "ah")

#Create tidy text format: Unnested, Unsummarized, -Undesirables, Stop and Short words
prince_tidy <- prince_data %>%
  unnest_tokens(word, lyrics) %>% #Break the lyrics into individual words
  filter(!word %in% undesirable_words) %>% #Remove undesirables
  filter(!nchar(word) < 3) %>% #Words like "ah" or "oo" used in music
  anti_join(stop_words) #Data provided by the tidytext package
glimpse(prince_tidy) #From `dplyr`, better than `str()`.
 ## Observations: 76,116
## Variables: 10
## $ song        <chr> "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", ...
## $ year        <int> 1992, 1992, 1992, 1992, 1992, 1992, 1992, 1992, 19...
## $ album       <chr> "Symbol", "Symbol", "Symbol", "Symbol", "Symbol", ...
## $ peak        <int> 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,...
## $ us_pop      <chr> "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", ...
## $ us_rnb      <chr> "61", "61", "61", "61", "61", "61", "61", "61", "6...
## $ decade      <chr> "1990s", "1990s", "1990s", "1990s", "1990s", "1990...
## $ chart_level <chr> "Top 10", "Top 10", "Top 10", "Top 10", "Top 10", ...
## $ charted     <chr> "Charted", "Charted", "Charted", "Charted", "Chart...
## $ word        <chr> "watch", "fall", "stand", "love", "smoke", "intell...

Votre nouveau jeu prince_tidy est désormais tokenisé : un mot par ligne, avec la chanson d’origine associée. Vous avez un data frame de 76 116 mots et 10 colonnes.

Statistiques descriptives

Si vous n’avez pas lu la Partie un, prenez un instant pour parcourir quelques graphiques de synthèse sur l’ensemble du jeu. Nous utiliserons des visuels originaux via ggplot2, circlize et yarrr.

Tout est « shipshape » : nombre de mots par chanson

Un pirate dirait shipshape quand tout est en ordre, propre et net. Voici donc une vue intéressante des données « tidy » montrant la diversité lexicale, autrement dit le vocabulaire, au fil du temps. Un pirate plot est une méthode avancée pour représenter une variable dépendante continue (p. ex. le nombre de mots) en fonction d’une variable catégorielle (p. ex. la décennie). Il combine données brutes, statistiques descriptives et inférentielles en un seul graphique efficace. Consultez cet excellent billet sur pirateplot() du package yarrr.

Créez le data frame word_summary qui calcule le nombre de mots distincts par chanson. Plus les paroles sont variées, plus le vocabulaire est riche. Penser les données ainsi vous prépare à l’analyse au niveau mot. Remplacez la décennie par "NONE" pour les titres sans date de sortie et renommez quelques champs via select().

word_summary <- prince_tidy %>%
  mutate(decade = ifelse(is.na(decade),"NONE", decade)) %>%
  group_by(decade, song) %>%
  mutate(word_count = n_distinct(word)) %>%
  select(song, Released = decade, Charted = charted, word_count) %>%
  distinct() %>% #To obtain one record per song
  ungroup()

pirateplot(formula =  word_count ~ Released + Charted, #Formula
   data = word_summary, #Data frame
   xlab = NULL, ylab = "Song Distinct Word Count", #Axis labels
   main = "Lexical Diversity Per Decade", #Plot title
   pal = "google", #Color scheme
   point.o = .2, #Points
   avg.line.o = 1, #Turn on the Average/Mean line
   theme = 0, #Theme
   point.pch = 16, #Point `pch` type
   point.cex = 1.5, #Point size
   jitter.val = .1, #Turn on jitter to see the songs better
   cex.lab = .9, cex.names = .7) #Axis label size

diversité lexicale par décennie

Chaque disque coloré de ce pirate plot représente une chanson. La zone rouge dense avec la valeur "NONE" montre qu’un grand nombre de morceaux du jeu n’ont pas de date de sortie. On observe une légère hausse du nombre de mots uniques par chanson dans les premières décennies : la ligne horizontale solide indique la moyenne de la décennie. C’est important quand vous commencerez à analyser l’évolution du sentiment dans le temps. Le vocabulaire devient plus révélateur au fil de la carrière de Prince.

Je vous encourage à creuser les pirate plots : nous n’avons fait qu’effleurer le sujet.

Toute l’année : nombre de titres par an

Les graphes circulaires offrent une manière originale de visualiser des relations complexes (ou simples) entre catégories. (Et les disques étaient ronds !) Le graphique ci-dessous est un simple histogramme circulaire avec coord_polar() de ggplot2, montrant le nombre relatif de chansons par année. Beaucoup de code pour un graph simple, mais ça vaut le coup. Un exemple détaillé est disponible dans la R Graph Gallery ici. Comme pour les pirate plots, ce n’est qu’une introduction à ce que vous pouvez faire avec les graphiques circulaires.

Nombre de chansons par an
songs_year <- prince_data %>%
  select(song, year) %>%
  group_by(year) %>%
  summarise(song_count = n())

id <- seq_len(nrow(songs_year))
songs_year <- cbind(songs_year, id)
label_data = songs_year
number_of_bar = nrow(label_data) #Calculate the ANGLE of the labels
angle = 90 - 360 * (label_data$id - 0.5) / number_of_bar #Center things
label_data$hjust <- ifelse(angle < -90, 1, 0) #Align label
label_data$angle <- ifelse(angle < -90, angle + 180, angle) #Flip angle
ggplot(songs_year, aes(x = as.factor(id), y = song_count)) +
  geom_bar(stat = "identity", fill = alpha("purple", 0.7)) +
  geom_text(data = label_data, aes(x = id, y = song_count + 10, label = year, hjust = hjust), color = "black", alpha = 0.6, size = 3, angle =  label_data$angle, inherit.aes = FALSE ) +
  coord_polar(start = 0) +
  ylim(-20, 150) + #Size of the circle
  theme_minimal() +
  theme(axis.text = element_blank(),
        axis.title = element_blank(),
        panel.grid = element_blank(),
        plot.margin = unit(rep(-4,4), "in"),
        plot.title = element_text(margin = margin(t = 10, b = -10)))

chansons sans dates de sortie

Voyez l’espace vide tout en haut ? C’est le signe des centaines de chansons sans dates de sortie repérées avec le pirate plot. Avec un tel volume de titres non datés, le graphique perdrait en lisibilité s’ils étaient inclus. Les années manquantes indiquent celles sans sortie. Les années les plus prolifiques : 1996 et 1998.

Envie de savoir pourquoi ? Lisez la suite !

Accords : titres classés par décennie

Le graphique suivant montre la relation entre la décennie de sortie et la présence ou non au Billboard. Utiliser un chordDiagram() pour une analyse musicale était tentant ! Cet outil graphique provient du très beau package circlize de Zuguang Gu. Le graphe est séparé en deux catégories : classement (haut) et décennie (bas). Les deux catégories sont séparées par de larges espaces, avec de plus petits espaces entre les valeurs. Les commentaires de haut niveau dans le code ci-dessous peuvent être complétés par davantage de détails ici.

decade_chart <-  prince_data %>%
  filter(decade != "NA") %>% #Remove songs without release dates
  count(decade, charted)  #Get SONG count per chart level per decade. Order determines top or bottom.

circos.clear() #Very important - Reset the circular layout parameters!
grid.col = c("1970s" = my_colors[1], "1980s" = my_colors[2], "1990s" = my_colors[3], "2000s" = my_colors[4], "2010s" = my_colors[5], "Charted" = "grey", "Uncharted" = "grey") #assign chord colors
# Set the global parameters for the circular layout. Specifically the gap size
circos.par(gap.after = c(rep(5, length(unique(decade_chart[[1]])) - 1), 15,
                         rep(5, length(unique(decade_chart[[2]])) - 1), 15))

chordDiagram(decade_chart, grid.col = grid.col, transparency = .2)
title("Relationship Between Chart and Decade")

relation entre classement et décennie

Ce diagramme circulaire peut sembler complexe au premier regard, mais il illustre clairement le nombre de chansons par décennie et par statut de classement. On voit que Prince commence sa carrière dans les années 1970 avec peu de sorties, dont certaines classées. En comparant les années 1980 aux années 1990, on constate davantage de sorties dans les années 1990, mais plus de titres classés dans les années 1980. Dans les années 2000, peu de succès commerciaux ; dans les années 2010, aucun hit.

Lexiques et paroles

Dans cette section, vous allez :

  • Explorer les lexiques de sentiment et leurs volumes
  • Identifier combien de mots des paroles figurent dans ces lexiques
  • Examiner certains mots et leurs formes
  • Envisager des traitements de données complémentaires

Explorer les lexiques de sentiment

Le package tidytext inclut un jeu sentiments qui regroupe plusieurs lexiques. Ces lexiques sont des dictionnaires associant des mots à une catégorie ou une valeur de sentiment. tidytext propose trois lexiques généralistes :

  • AFINN : attribue à chaque mot un score de -5 à 5, les scores négatifs signalant un sentiment négatif et les positifs un sentiment positif
  • Bing : classe les mots en catégories positive et négative
  • NRC : classe les mots dans une ou plusieurs des dix catégories : positif, négatif, colère, anticipation, dégoût, peur, joie, tristesse, surprise et confiance

Pour examiner les lexiques, créez un data frame new_sentiments. Excluez un lexique financier, créez un champ de sentiment binaire (dit polar) pour AFINN en convertissant le score numérique en positive ou negative, et ajoutez un champ contenant le nombre de mots distincts par lexique.

new_sentiments comporte une colonne avec les différentes catégories ; pour mieux visualiser les volumes par lexique et par catégorie, utilisez spread() de tidyr pour pivoter ces catégories en colonnes. Profitez de knitr et kableExtra via la fonction my_kable_styling() créée plus tôt. Ajoutez de la couleur avec color_tile() et color_bar() de formattable pour obtenir une table joliment formatée. Imprimez-la et comparez les différences entre lexiques.

new_sentiments <- sentiments %>% #From the tidytext package
  filter(lexicon != "loughran") %>% #Remove the finance lexicon
  mutate( sentiment = ifelse(lexicon == "AFINN" & score >= 0, "positive",
                              ifelse(lexicon == "AFINN" & score < 0,
                                     "negative", sentiment))) %>%
  group_by(lexicon) %>%
  mutate(words_in_lexicon = n_distinct(word)) %>%
  ungroup()

new_sentiments %>%
  group_by(lexicon, sentiment, words_in_lexicon) %>%
  summarise(distinct_words = n_distinct(word)) %>%
  ungroup() %>%
  spread(sentiment, distinct_words) %>%
  mutate(lexicon = color_tile("lightblue", "lightblue")(lexicon),
         words_in_lexicon = color_bar("lightpink")(words_in_lexicon)) %>%
  my_kable_styling(caption = "Word Counts Per Lexicon")
Word Counts Per Lexicon
lexicon words_in_lexicon anger anticipation disgust fear joy negative positive sadness surprise trust
AFINN 2476 NA NA NA NA NA 1597 879 NA NA NA
bing 6785 NA NA NA NA NA 4782 2006 NA NA NA
nrc 6468 1247 839 1058 1476 689 3324 2312 1191 534 1231

Le tableau ci-dessus vous donne une idée de la taille et de la structure de chaque lexique.

Mots en commun

Pour savoir quel lexique convient le mieux aux paroles, étudiez le ratio de correspondance des mots présents à la fois dans le lexique et dans les paroles. Rappel : prince_tidy contient 76 116 mots au total et 7851 mots distincts.

Combien de ces mots figurent effectivement dans les lexiques ?

Faites un inner_join() entre prince_tidy et new_sentiments, puis regroupez par lexique. Le lexique NRC comprend 10 catégories, et un mot peut appartenir à plusieurs catégories : par exemple, un mot peut être négatif et triste. Utilisez donc n_distinct() dans summarise() pour obtenir le nombre de mots distincts par lexique.

prince_tidy %>%
  mutate(words_in_lyrics = n_distinct(word)) %>%
  inner_join(new_sentiments) %>%
  group_by(lexicon, words_in_lyrics, words_in_lexicon) %>%
  summarise(lex_match_words = n_distinct(word)) %>%
  ungroup() %>%
  mutate(total_match_words = sum(lex_match_words), #Not used but good to have
         match_ratio = lex_match_words / words_in_lyrics) %>%
  select(lexicon, lex_match_words,  words_in_lyrics, match_ratio) %>%
  mutate(lex_match_words = color_bar("lightpink")(lex_match_words),
         lexicon = color_tile("lightgreen", "lightgreen")(lexicon)) %>%
  my_kable_styling(caption = "Lyrics Found In Lexicons")
Lyrics Found In Lexicons
lexicon lex_match_words words_in_lyrics match_ratio
AFINN 770 7851 0.0980767
bing 1185 7851 0.1509362
nrc 1678 7851 0.2137307

Le lexique NRC comporte davantage de mots distincts des paroles que AFINN ou Bing. Notez que la somme des ratios de correspondance est faible. Aucun lexique ne peut (ni ne doit) contenir tous les mots. Beaucoup sont neutres et n’ont pas de sentiment associé. Par exemple, 2000 est en général neutre, donc absent des lexiques. Pourtant, souvenez-vous des peurs liées à l’an 2000 : avions supposés tomber du ciel, ordinateurs censés s’arrêter... Une crainte associée existe bien dans la chanson, mais n’est pas capturée par une analyse fondée sur des lexiques.

Quelques raisons pour lesquelles un mot n’apparaît pas dans un lexique :

  • Tous les mots n’ont pas un sentiment.
  • Les lexiques ont été conçus pour d’autres types de texte, pas pour des paroles.
  • La forme du mot peut ne pas y figurer. Par exemple, strong pourrait être présent, mais pas strongly. Il peut donc falloir plus de nettoyage ! (Nous y reviendrons.)

Ne me croyez pas sur parole

Examinez quelques mots précis des chansons de Prince qui semblent influencer le sentiment. Sont-ils dans tous les lexiques ?

new_sentiments %>%
  filter(word %in% c("dark", "controversy", "gangster",
                     "discouraged", "race")) %>%
  arrange(word) %>% #sort
  select(-score) %>% #remove this field
  mutate(word = color_tile("lightblue", "lightblue")(word),
         words_in_lexicon = color_bar("lightpink")(words_in_lexicon),
         lexicon = color_tile("lightgreen", "lightgreen")(lexicon)) %>%
  my_kable_styling(caption = "Specific Words")
Specific Words
word sentiment lexicon words_in_lexicon
controversy negative nrc 6468
controversy negative bing 6785
dark sadness nrc 6468
dark negative bing 6785
discouraged negative AFINN 2476
gangster negative bing 6785

Controversy et dark apparaissent chez NRC et Bing, mais gangster seulement chez Bing. Race n’apparaît pas alors qu’il s’agit d’un thème central chez Prince. Est-il pour autant facile à associer à un sentiment ? Notez aussi qu’AFINN, beaucoup plus réduit, ne contient qu’un seul de ces mots.

Formes de mots

Voyons un exemple plus complexe. La sexualité est un thème récurrent chez Prince. Comment une analyse basée sur des lexiques prédéfinis gère-t-elle les différentes formes d’un mot ? Voici toutes les occurrences liées à la racine sex dans les paroles. Comparez avec Bing et NRC pour voir où il y a correspondance.

my_word_list <- prince_data %>%
  unnest_tokens(word, lyrics) %>%
  filter(grepl("sex", word)) %>% #Use `grepl()` to find the substring `"sex"`
  count(word) %>%
  select(myword = word, n) %>% #Rename word
  arrange(desc(n))

new_sentiments %>%
  #Right join gets all words in `my_word_list` to show nulls
  right_join(my_word_list, by = c("word" = "myword")) %>%
  filter(word %in% my_word_list$myword) %>%
  mutate(word = color_tile("lightblue", "lightblue")(word),
          instances = color_tile("lightpink", "lightpink")(n),
          lexicon = color_tile("lightgreen", "lightgreen")(lexicon)) %>%
  select(-score, -n) %>% #Remove these fields
  my_kable_styling(caption = "Dependency on Word Form")
Dependency on Word Form
word sentiment lexicon words_in_lexicon instances
sexy positive bing 6785 220
sexy positive AFINN 2476 220
sex anticipation nrc 6468 185
sex joy nrc 6468 185
sex positive nrc 6468 185
sex trust nrc 6468 185
superfunkycalifragisexy NA NA NA 19
lovesexy NA NA NA 16
sexual NA NA NA 11
sexuality NA NA NA 11
sexiness NA NA NA 2
sexually NA NA NA 2
sexe NA NA NA 1
sexed NA NA NA 1
sexier NA NA NA 1
superfunkycalifraagisexy NA NA NA 1
superfunkycalifragisexi NA NA NA 1

Remarquez que Prince utilise souvent sexy, mais ce mot n’existe pas sous cette forme dans NRC. Le mot sex existe dans NRC mais pas dans Bing. Que se passerait-il si vous regardiez les stems ou racines ? Votre texte peut contenir un passé, un pluriel, un adverbe d’un mot racine, mais la forme n’existe dans aucun lexique. Comment gérer cela ?

Davantage de préparation ?

Il se peut que vous ayez besoin de quelques étapes supplémentaires. Trois techniques à envisager avant une analyse de sentiment :

  • Stemming : suppression des suffixes pour retrouver l’origine commune
  • Lemmatisation : réduction des mots fléchis (ou dérivés) à leur forme de base
  • Remplacement de mots : substituer des synonymes plus courants

Un concept avancé consiste à remplacer par des synonymes (pairs sémantiquement proches) ou hyperonymes (parents communs) plus fréquents. Ces termes apparaissent dans le lexique et augmentent le taux de correspondance. Nous n’avons pas la place pour couvrir ces traitements, mais c’est à considérer.

Défi : renseignez-vous sur la création des lexiques. En existe-t-il déjà un mieux adapté aux paroles ? Si le sujet vous passionne, que faudrait-il pour construire votre propre lexique ? Quelle différence entre une approche par classifieur et une approche par lexique ?

Analyse détaillée

Maintenant que vous avez compris le jeu de données et les lexiques, vous pouvez les joindre pour analyser. Étapes générales :

  • Créer des jeux de données par lexique
  • Observer le sentiment polar sur l’ensemble des chansons
  • Examiner l’évolution du sentiment dans le temps
  • Valider contre des événements de la vie de Prince
  • Étudier le sentiment au niveau chanson
  • Voir comment les paires de mots influencent le sentiment

Créer les jeux de sentiment

Commencez par créer des jeux de sentiment spécifiques à chaque lexique via un inner_join() sur la fonction get_sentiments(). Passez le nom du lexique à chaque appel. Pour cet exercice, utilisez Bing pour le binaire et NRC pour le catégoriel. Comme un mot peut apparaître dans plusieurs catégories NRC (négatif/peur, positif/joie), créez aussi un sous-ensemble sans les catégories positive et negative pour plus tard.

prince_bing <- prince_tidy %>%
  inner_join(get_sentiments("bing"))
prince_nrc <- prince_tidy %>%
  inner_join(get_sentiments("nrc"))
prince_nrc_sub <- prince_tidy %>%
  inner_join(get_sentiments("nrc")) %>%
  filter(!sentiment %in% c("positive", "negative"))

Dans l’ambiance : sentiment global

Dans l’analyse détaillée, vous étudierez différents niveaux : toutes chansons, niveau classement, décennie, mot. Commencez par tracer l’analyse NRC sur l’ensemble du jeu.

(Pour le fun, j’ai utilisé les packages memery et magick pour ajouter des images (memes) aux graphiques.)

nrc_plot <- prince_nrc %>%
  group_by(sentiment) %>%
  summarise(word_count = n()) %>%
  ungroup() %>%
  mutate(sentiment = reorder(sentiment, word_count)) %>%
  #Use `fill = -word_count` to make the larger bars darker
  ggplot(aes(sentiment, word_count, fill = -word_count)) +
  geom_col() +
  guides(fill = FALSE) + #Turn off the legend
  theme_lyrics() +
  labs(x = NULL, y = "Word Count") +
  scale_y_continuous(limits = c(0, 15000)) + #Hard code the axis limit
  ggtitle("Prince NRC Sentiment") +
  coord_flip()

img <- "prince_background2.jpg" #Load the background image
lab <- ""  #Turn off the label
#Overlay the plot on the image and create the meme file
meme(img, lab, "meme_nrc.jpg", inset = nrc_plot)
#Read the file back in and display it!
nrc_meme <- image_read("meme_nrc.jpg")
plot(nrc_meme)

sentiment nrc prince

Pour les paroles de Prince, NRC penche fortement vers le positif. Mais les mots associés à disgust ou anger figurent-ils aussi dans la catégorie negative ? Cela mérite vérification.

Regardons maintenant le sentiment global selon Bing. Parmi les 1185 mots distincts des paroles de Prince présents dans Bing, combien sont positifs et combien négatifs ?

bing_plot <- prince_bing %>%
  group_by(sentiment) %>%
  summarise(word_count = n()) %>%
  ungroup() %>%
  mutate(sentiment = reorder(sentiment, word_count)) %>%
  ggplot(aes(sentiment, word_count, fill = sentiment)) +
  geom_col() +
  guides(fill = FALSE) +
  theme_lyrics() +
  labs(x = NULL, y = "Word Count") +
  scale_y_continuous(limits = c(0, 8000)) +
  ggtitle("Prince Bing Sentiment") +
  coord_flip()

img1 <- "prince_background1.jpg"
lab1 <- ""
meme(img1, lab1, "meme_bing.jpg", inset = bing_plot)
x <- image_read("meme_bing.jpg")
plot(x)

sentiment bing prince

Se pourrait-il qu’avec Bing, le positif et le négatif s’équilibrent dans l’ensemble des chansons ? Le sentiment se neutralise-t-il à l’échelle d’un corpus trop vaste ? Difficile à dire ; découpez en « tranches » et observez.

En acoustique, on parle d’annulation de phase lorsque deux signaux de même fréquence sont en opposition de phase et s’annulent, par exemple lors de l’enregistrement d’une batterie avec deux micros. Résultat : un silence total à cette fréquence. Comment cela pourrait-il s’appliquer à une analyse de sentiment sur de grands jeux ? À méditer.

Professeur de linguistique : « En anglais, une double négation devient une affirmation. En russe, une double négation reste une négation. Mais il n’existe aucune langue où deux affirmations produisent une négation. » Un étudiant réplique : « Ouais, bien sûr. »

Montez le son : niveau classement

Affinez l’analyse au niveau du classement avec le lexique Bing. Créez un graphique du sentiment polar par niveau de classement. Utilisez spread() pour séparer les sentiments en colonnes et mutate() pour créer un champ polarity (positive − negative) et un percent_positive (positive / total des sentiments × 100). Pour le graphe de polarité, ajoutez une yintercept avec geom_hline(). Affichez les deux graphes côte à côte avec grid.arrange().

prince_polarity_chart <- prince_bing %>%
  count(sentiment, chart_level) %>%
  spread(sentiment, n, fill = 0) %>%
  mutate(polarity = positive - negative,
    percent_positive = positive / (positive + negative) * 100)

#Polarity by chart
plot1 <- prince_polarity_chart %>%
  ggplot( aes(chart_level, polarity, fill = chart_level)) +
  geom_col() +
  scale_fill_manual(values = my_colors[3:5]) +
  geom_hline(yintercept = 0, color = "red") +
  theme_lyrics() + theme(plot.title = element_text(size = 11)) +
  xlab(NULL) + ylab(NULL) +
  ggtitle("Polarity By Chart Level")

#Percent positive by chart
plot2 <- prince_polarity_chart %>%
  ggplot( aes(chart_level, percent_positive, fill = chart_level)) +
  geom_col() +
  scale_fill_manual(values = c(my_colors[3:5])) +
  geom_hline(yintercept = 0, color = "red") +
  theme_lyrics() + theme(plot.title = element_text(size = 11)) +
  xlab(NULL) + ylab(NULL) +
  ggtitle("Percent Positive By Chart Level")

grid.arrange(plot1, plot2, ncol = 2)

polarité et pourcentage positif par niveau de classement

Ces résultats suggèrent-ils que les titres classés sont en général plus positifs que négatifs ? Qu’est-ce que cela dit de ce que le public souhaite entendre ? Peut-on l’affirmer ? En regardant le positif relatif au total, les titres classés semblent légèrement plus positifs. Intéressant sachant que Bing contient lui-même plus de mots négatifs que positifs.

Fonte polaire : « So Blue »

Puisque vous examinez le sentiment sous un angle polar, vérifiez s’il évolue (humour de data geek). Utilisez geom_smooth() avec la méthode loess pour une courbe lissée, et un autre geom_smooth() avec lm pour une tendance linéaire.

prince_polarity_year <- prince_bing %>%
  count(sentiment, year) %>%
  spread(sentiment, n, fill = 0) %>%
  mutate(polarity = positive - negative,
    percent_positive = positive / (positive + negative) * 100)

polarity_over_time <- prince_polarity_year %>%
  ggplot(aes(year, polarity, color = ifelse(polarity >= 0,my_colors[5],my_colors[4]))) +
  geom_col() +
  geom_smooth(method = "loess", se = FALSE) +
  geom_smooth(method = "lm", se = FALSE, aes(color = my_colors[1])) +
  theme_lyrics() + theme(plot.title = element_text(size = 11)) +
  xlab(NULL) + ylab(NULL) +
  ggtitle("Polarity Over Time")

relative_polarity_over_time <- prince_polarity_year %>%
  ggplot(aes(year, percent_positive , color = ifelse(polarity >= 0,my_colors[5],my_colors[4]))) +
  geom_col() +
  geom_smooth(method = "loess", se = FALSE) +
  geom_smooth(method = "lm", se = FALSE, aes(color = my_colors[1])) +
  theme_lyrics() + theme(plot.title = element_text(size = 11)) +
  xlab(NULL) + ylab(NULL) +
  ggtitle("Percent Positive Over Time")

grid.arrange(polarity_over_time, relative_polarity_over_time, ncol = 2)

polarité et pourcentage positif dans le temps

Quelques extrêmes ont été ajustés sur le second graphe, mais la tendance de fond de la polarité dans le temps est négative dans les deux cas.

Bague d’humeur

Exploitez à nouveau la puissance de chordDiagram() pour étudier les relations entre les sentiments NRC et les décennies. Les catégories de sentiment apparaissent en haut de l’anneau, les décennies en bas.

grid.col = c("1970s" = my_colors[1], "1980s" = my_colors[2], "1990s" = my_colors[3], "2000s" = my_colors[4], "2010s" = my_colors[5], "anger" = "grey", "anticipation" = "grey", "disgust" = "grey", "fear" = "grey", "joy" = "grey", "sadness" = "grey", "surprise" = "grey", "trust" = "grey")

decade_mood <-  prince_nrc %>%
  filter(decade != "NA" & !sentiment %in% c("positive", "negative")) %>%
  count(sentiment, decade) %>%
  group_by(decade, sentiment) %>%
  summarise(sentiment_sum = sum(n)) %>%
  ungroup()

circos.clear()
#Set the gap size
circos.par(gap.after = c(rep(5, length(unique(decade_mood[[1]])) - 1), 15,
                         rep(5, length(unique(decade_mood[[2]])) - 1), 15))
chordDiagram(decade_mood, grid.col = grid.col, transparency = .2)
title("Relationship Between Mood and Decade")

relation entre humeur et décennie

On visualise ici le nombre de mots par catégorie NRC et par décennie. Beaucoup d’informations pour un petit graphique, mais il révèle énormément de relations entre catégories et temporalité. Ces diagrammes sont extrêmement personnalisables.

Sentiment en temps réel

Après toutes les excellentes formations suivies sur DataCamp, vous avez sans doute envie d’appliquer ces compétences au réel. Cartographiez maintenant votre analyse des paroles de Prince à des éléments concrets sur une période. Restez néanmoins prudent : le sujet est complexe et notre analyse simplifiée.

J’ai constitué une liste d’événements de la vie de Prince, à partir de sources populaires comme Rolling Stone, Biography.com, etc. J’ai sélectionné des années marquantes qui correspondent à des chansons datées dans notre jeu. Lisez ces événements depuis princeEvents.csv. Puis, à partir de prince_bing et de spread(), calculez un score de polarité par année. Rejoignez le data frame events et créez un champ de sentiment pour colorer vos barres. Comme toujours, utilisez coord_flip() avec de longues étiquettes.

events <- read.csv('princeEvents.csv', stringsAsFactors = FALSE)

year_polarity_bing <- prince_bing %>%
  group_by(year, sentiment) %>%
  count(year, sentiment) %>%
  spread(sentiment, n) %>%
  mutate(polarity = positive - negative,
         ratio = polarity / (positive + negative)) #use polarity ratio in next graph

 events %>%
   #Left join gets event years with no releases
   left_join(year_polarity_bing) %>%
   filter(event != " ") %>% #Account for bad data
   mutate(event = reorder(event, year), #Sort chart by desc year
           sentiment = ifelse(positive > negative,
                              "positive", "negative")) %>%
   ggplot(aes(event, polarity, fill = sentiment)) +
   geom_bar(stat = "identity") +
   theme_minimal() + theme(legend.position = "none") +
   xlab(NULL) +
   ggtitle("Sentiment by Events") +
   coord_flip()

sentiment par événements

Conseil : vous trouverez princeEvents.csv ici.

Comparer les événements au sentiment est fascinant. Bien que subjectifs, je les ai trouvés très corrélés. D’accord, j’ai créé ce jeu d’événements ; j’aurais pu le biaiser. Mais j’ai croisé plusieurs sources par année pour retenir les faits les plus rapportés. Ces résultats devraient vous motiver à pousser l’analyse au niveau mot pour comprendre de quoi Prince chantait.

The Black Album : 1994 – 1996

En 1994, Prince sort The Black Album pour reconquérir son public afro-américain. Étant donné que 1994 et les deux années suivantes ressortent de l’analyse, regardez les mots les plus fréquents correspondant au lexique NRC sur cette période, avec geom_label_repel() du package ggrepel et geom_point(). Le graphique est un peu délicat ; testez différents réglages du code ci-dessous.

plot_words_94_96 <- prince_nrc %>%
  filter(year %in% c("1994", "1995", "1996")) %>%
  group_by(sentiment) %>%
  count(word, sort = TRUE) %>%
  arrange(desc(n)) %>%
  slice(seq_len(8)) %>% #consider top_n() from dplyr also
  ungroup()

plot_words_94_96 %>%
  #Set `y = 1` to just plot one variable and use word as the label
  ggplot(aes(word, 1, label = word, fill = sentiment )) +
  #You want the words, not the points
  geom_point(color = "transparent") +
  #Make sure the labels don't overlap
  geom_label_repel(force = 1,nudge_y = .5,  
                   direction = "y",
                   box.padding = 0.04,
                   segment.color = "transparent",
                   size = 3) +
  facet_grid(~sentiment) +
  theme_lyrics() +
  theme(axis.text.y = element_blank(), axis.text.x = element_blank(),
        axis.title.x = element_text(size = 6),
        panel.grid = element_blank(), panel.background = element_blank(),
        panel.border = element_rect("lightgray", fill = NA),
        strip.text.x = element_text(size = 9)) +
  xlab(NULL) + ylab(NULL) +
  ggtitle("1994 - 1996 NRC Sentiment") +
  coord_flip()

1994-1996 nrc sentiment

En 1994, Prince déclare à Rolling Stone :

« Quand vous empêchez un homme de rêver, il devient esclave. C’est là que j’en étais. »

Il apparaît alors en public avec le mot « slave » écrit en gros sur le visage, affichant son rejet de sa maison de disques. Il change aussi son nom pour un symbole. Dans le graphe, observez-vous les mots "slave", "emancipation", "black" et "change" ?

(Rappel : il ne s’agit pas que de Prince. Appliquez cette méthode à tout texte ! Par exemple, quels étaient les mots les plus associés à la confiance dans les discours du président Trump durant sa première année ?)

Cette fois, c’est personnel : 1998

Le graphique Sentiment par événements indiquait qu’entre 1996 et 1998, Prince s’est marié, a perdu deux enfants, et aurait prédit le 11 septembre sur scène. (Cherchez son concert de 1998 aux Pays-Bas sur YouTube pour juger par vous-même.) Reprenez la même démarche que précédemment pour extraire les 10 mots principaux de chaque catégorie NRC en 1998.

plot_words_1998 <- prince_nrc %>%
  filter(year == "1998") %>%
  group_by(sentiment) %>%
  count(word, sort = TRUE) %>%
  arrange(desc(n)) %>%
  slice(seq_len(10)) %>%
  ungroup()

#Same comments as previous graph
plot_words_1998 %>%
  ggplot(aes(word, 1, label = word, fill = sentiment )) +
  geom_point(color = "transparent") +
  geom_label_repel(force = 1,nudge_y = .5,  
                   direction = "y",
                   box.padding = 0.05,
                   segment.color = "transparent",
                   size = 3) +
  facet_grid(~sentiment) +
  theme_lyrics() +
  theme(axis.text.y = element_blank(), axis.text.x = element_blank(),
        axis.title.x = element_text(size = 6),
        panel.grid = element_blank(), panel.background = element_blank(),
        panel.border = element_rect("lightgray", fill = NA),
        strip.text.x = element_text(size = 9)) +
  xlab(NULL) + ylab(NULL) +
  ggtitle("1998 NRC Sentiment") +
  coord_flip()

1998 nrc sentiment

Vu les drames personnels et la menace terroriste, des mots comme "suicide", "waste", "mad", "hurt", et bien d’autres non affichés ici reflètent fidèlement les événements. Ces mots sont puissants et éclairent réellement l’humeur de l’époque.

Sur le radar : graphiques radar

Un autre bon moyen de comparer les sentiments par catégories est le graphique radar (ou « toile d’araignée »), via le package radarchart. Utile pour repérer les variables aux valeurs proches ou des valeurs atypiques.

Nous déclinons l’analyse à trois niveaux : année, classement, décennie. (Pour gagner de la place, je ne montre ici que le code pour les années.)

  • Utilisez prince_nrc_sub sans les sentiments positive et negative afin de rendre les autres plus lisibles. Calculez d’abord le nombre total de mots par sentiment et par année, ainsi que le total annuel, puis obtenez un pourcentage (mots d’un sentiment / total annuel × 100).
  • Filtrez pour 1978, 1994, 1995 et supprimez les champs superflus avec select().
  • Enfin, spread() l’année et la valeur de pourcentage en plusieurs colonnes afin d’avoir une ligne par sentiment et une colonne par année. Utilisez ensuite chartJSRadar() pour générer un widget HTML interactif. Vous pouvez afficher les libellés au survol.
#Get the count of words per sentiment per year
year_sentiment_nrc <- prince_nrc_sub %>%
  group_by(year, sentiment) %>%
  count(year, sentiment) %>%
  select(year, sentiment, sentiment_year_count = n)

#Get the total count of sentiment words per year (not distinct)
total_sentiment_year <- prince_nrc_sub %>%
  count(year) %>%
  select(year, year_total = n)

#Join the two and create a percent field
year_radar_chart <- year_sentiment_nrc %>%
  inner_join(total_sentiment_year, by = "year") %>%
  mutate(percent = sentiment_year_count / year_total * 100 ) %>%
  filter(year %in% c("1978","1994","1995")) %>%
  select(-sentiment_year_count, -year_total) %>%
  spread(year, percent) %>%
  chartJSRadar(showToolTipLabel = TRUE,
               main = "NRC Years Radar")

Faites défiler pour comparer les graphes, je les ai laissés en pleine taille. Intéressant : sur un sous-ensemble plus petit comme year, on distingue mieux la variance de chaque sentiment. Vous noterez aussi que, en pourcentage, « joy » domine sur tous les graphiques (notamment au début de la carrière de Prince en 1978).

Sign O’ the Times

Spotify utilise le machine learning pour recommander des titres à ses 140 millions d’utilisateurs actifs. Il n’utilise pas encore les paroles. Imaginez s’il le faisait… Approfondissons maintenant l’humeur de chansons précises.

En 1987, Prince écrit "Sign O' the Times". Un article de Billboard signé Kenneth Partridge dit :

« En abordant le SIDA, les gangs, la drogue, les catastrophes naturelles, et même Challenger, Prince reste calme et détaché. 'Sign O' the Times' est un état des lieux, pas un appel à l’action. Sa solution, ce n’est pas de battre le pavé ni d’appeler votre député. »

Comment une machine interpréterait-elle l’humeur de ce morceau ? Fortement émotionnelle ou plutôt factuelle ? Représente-t-elle Prince en tant que personne ou la société en général ? Tracez les catégories NRC avec ggplot2.

prince_nrc %>%
  filter(song %in% "sign o the times") %>%
  group_by(sentiment) %>%
  summarise(word_count = n()) %>%
  ungroup() %>%
  mutate(sentiment = reorder(sentiment, word_count)) %>%
  ggplot(aes(sentiment, word_count, fill = -word_count)) +
  geom_col() +
  guides(fill = FALSE) +
  theme_minimal() + theme_lyrics() +
  labs(x = NULL, y = "Word Count") +
  ggtitle("Sign O' The Times NRC Sentiment") +
  coord_flip()

sign o' the times nrc sentiment

Toujours subjectif, mais vos résultats semblent confirmer que Prince « reste calme et détaché », comme l’écrit Partridge. On peut l’interpréter au vu de la prédominance des mots d’anticipation par rapport aux catégories davantage émotionnelles comme sadness, fear et anger. Cet article précise que ces trois catégories sont des émotions tandis que l’anticipation est un sentiment. L’auteur de Billboard qualifie la chanson « d’état des lieux, presque dénué d’émotion ». La machine semble d’accord. Et vous ? Même si c’est un tutoriel R, l’analyse de sentiment n’est pas que technique : on dit qu’elle est à la croisée de l’IA et de la psychologie.

Avec ggplot2, créez un graphique légèrement différent pour visualiser les mots par catégorie.

prince_tidy %>%
  filter(song %in% 'sign o the times') %>%
  distinct(word) %>%
  inner_join(get_sentiments("nrc")) %>%
  ggplot(aes(x = word, fill = sentiment)) +
  facet_grid(~sentiment) +
  geom_bar() + #Create a bar for each word per sentiment
  theme_lyrics() +
  theme(panel.grid.major.x = element_blank(),
        axis.text.x = element_blank()) + #Place the words on the y-axis
  xlab(NULL) + ylab(NULL) +
  ggtitle("Sign O' The Times Sentiment Words") +
  coord_flip()

sign o' the times sentiment words

Associez-vous ces mots aux thèmes cités par Billboard ? Lien direct : catastrophe de Challenger → "rocket", SIDA → "disease", drogues → "crack", catastrophes naturelles → "hurricane", gangs → "gang", etc.

On dirait presque que Partridge a écrit un peu de code R et produit ce graphique avant d’écrire son article.

D’autres chansons

Regardez les catégories de sentiment pour quelques autres titres aux noms évocateurs et voyez si la corrélation est au rendez-vous.

prince_nrc_sub %>%
  filter(song %in% c("so blue", "controversy", "raspberry beret",
                     "when doves cry", "the future", "1999")) %>%
  count(song, sentiment, year) %>%
  mutate(sentiment = reorder(sentiment, n), song = reorder(song, n)) %>%
  ggplot(aes(sentiment, n, fill = sentiment)) +
  geom_col() +
  facet_wrap(year ~ song, scales = "free_x", labeller = label_both) +
  theme_lyrics() +
  theme(panel.grid.major.x = element_blank(),
        axis.text.x = element_blank()) +
  labs(x = NULL, y = NULL) +
  ggtitle("NRC Sentiment Song Analysis") +
  coord_flip()

analyse de sentiment nrc par chanson

Les sentiments NRC montrent une forte anticipation et fear pour une chanson sur the future, ainsi que ces deux catégories plus une forte trust pour un titre sur la controversy. Les morceaux dominés par la tristesse collent aussi à leur titre.

Bigrammes par décennie

Jusqu’ici, nous avons analysé des unigrams (mots seuls). Mais si "love" est fréquent, que le précède-t-il ? Et que le suit ? Regarder des mots isolés peut être trompeur. Passons donc aux bigrammes (paires de mots).

Pratique : tidytext permet aussi de tokeniser en paires. Utilisez unnest_tokens() avec l’argument token ngrams et n = 2 pour ne garder que les bigrammes. Stockez le résultat dans prince_bigrams.

Le package tidyr peut séparer les bigrammes en mots individuels via separate(). Pour supprimer stop words et indésirables, séparez d’abord, filtrez ce que vous ne voulez pas, puis unite() pour reconstituer les paires. Ainsi, vous visualiserez facilement les bigrammes les plus fréquents par décennie. (Voir la Partie un pour slice() et row_number().)

prince_bigrams <- prince_data %>%
  unnest_tokens(bigram, lyrics, token = "ngrams", n = 2)

bigrams_separated <- prince_bigrams %>%
  separate(bigram, c("word1", "word2"), sep = " ")

bigrams_filtered <- bigrams_separated %>%
  filter(!word1 %in% stop_words$word) %>%
  filter(!word2 %in% stop_words$word) %>%
  filter(!word1 %in% undesirable_words) %>%
  filter(!word2 %in% undesirable_words)

#Because there is so much repetition in music, also filter out the cases where the two words are the same
bigram_decade <- bigrams_filtered %>%
  filter(word1 != word2) %>%
  filter(decade != "NA") %>%
  unite(bigram, word1, word2, sep = " ") %>%
  inner_join(prince_data) %>%
  count(bigram, decade, sort = TRUE) %>%
  group_by(decade) %>%
  slice(seq_len(7)) %>%
  ungroup() %>%
  arrange(decade, n) %>%
  mutate(row = row_number())
## Joining, by = c("song", "year", "album", "peak", "us_pop", "us_rnb", "decade", "chart_level", "charted")
bigram_decade %>%
  ggplot(aes(row, n, fill = decade)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~decade, scales = "free_y") +
  xlab(NULL) + ylab(NULL) +
  scale_x_continuous(  # This handles replacement of row
      breaks = bigram_decade$row, # Notice need to reuse data frame
      labels = bigram_decade$bigram) +
  theme_lyrics() +
  theme(panel.grid.major.x = element_blank()) +
  ggtitle("Bigrams Per Decade") +
  coord_flip()

bigrammes par décennie

Avec les bigrammes, on devine presque l’évolution des thèmes : du sexe, de la danse et de la romance vers la religion et les (rainbow) children. Pour info, un « rainbow baby » désigne parfois un enfant conçu après la perte d’un bébé. Fait intéressant : je n’ai pas trouvé de critique de l’album Rainbow Children de Prince qui mentionne cela.

Sentiment et bigrammes

Comment les bigrammes influencent-ils le sentiment ? Utilisons cette fois le lexique AFINN pour analyser les paires de mots, en observant la fréquence des mots à sentiment précédés de "not" ou d’autres négations.

AFINN <- get_sentiments("afinn")

not_words <- bigrams_separated %>%
  filter(word1 == "not") %>%
  inner_join(AFINN, by = c(word2 = "word")) %>%
  count(word2, score, sort = TRUE) %>%
  ungroup()

not_words %>%
  mutate(contribution = n * score) %>%
  arrange(desc(abs(contribution))) %>%
  head(20) %>%
  mutate(word2 = reorder(word2, contribution)) %>%
  ggplot(aes(word2, n * score, fill = n * score > 0)) +
  geom_col(show.legend = FALSE) +
  theme_lyrics() +
  xlab("Words preceded by \"not\"") +
  ylab("Sentiment score * Number of Occurrences") +
  ggtitle("Polar Sentiment of Words Preceded by Not") +
  coord_flip()

sentiment polar des mots précédés de not

Sur la première ligne, care obtient un faux positif car le "not" est ignoré par l’analyse à mot unique. Les bigrammes faux positifs compensent-ils les faux négatifs ?

Encore une question ouverte, mais un excellent sujet d’exploration. Conseil : vous pouvez aussi passer « trigrams » et n = 3 à unnest_tokens() pour analyser trois mots consécutifs.

Il existe d’autres mots de négation à considérer. Créons un graphe de réseau avec ggraph et igraph. Disposez les mots en nœuds reliés, avec les négations au centre. Créez l’objet à partir du jeu « tidy » avec graph_from_data_frame(), puis tracez via ggraph(). Mettez en valeur les nœuds principaux avec geom_edge_density(). Pour un exemple proche, voyez le livre de Julia Silge et David Robinson sur la fouille de texte « tidy ».

negation_words <- c("not", "no", "never", "without")

negation_bigrams <- bigrams_separated %>%
  filter(word1 %in% negation_words) %>%
  inner_join(AFINN, by = c(word2 = "word")) %>%
  count(word1, word2, score, sort = TRUE) %>%
  mutate(contribution = n * score) %>%
  arrange(desc(abs(contribution))) %>%
  group_by(word1) %>%
  slice(seq_len(20)) %>%
  arrange(word1,desc(contribution)) %>%
  ungroup()

bigram_graph <- negation_bigrams %>%
  graph_from_data_frame() #From `igraph`

set.seed(123)

a <- grid::arrow(type = "closed", length = unit(.15, "inches"))

ggraph(bigram_graph, layout = "fr") +
  geom_edge_link(alpha = .25) +
  geom_edge_density(aes(fill = score)) +
  geom_node_point(color = "purple1", size = 1) + #Purple for Prince!
  geom_node_text(aes(label = name),  repel = TRUE) +
  theme_void() + theme(legend.position = "none",
                       plot.title = element_text(hjust = 0.5)) +
  ggtitle("Negation Bigram Network")

réseau de bigrammes de négation

Vous voyez ici les paires associées aux négations. Si votre analyse repose sur les unigrams et que "alone" ressort négatif, le bigramme "not alone" aura l’effet inverse. Certains mots se connectent à plusieurs nœuds, visible sur ce type de visuel : par exemple, "never hurt" et "not hurt".

Comparaisons par paires

Puisque vous avez étudié les n-grammes, regardez la corrélation entre mots. Quels mots sont le plus corrélés ? Utilisez pairwise_count() du package widyr pour repérer les cooccurrences : le nombre de fois où chaque paire apparaît ensemble dans une chanson. widyr prend un jeu « tidy », le met temporairement en large, puis le remet en « tidy » pour la visualisation et l’analyse.

Pour rester simple, j’ai retenu quatre mots intéressants chez Prince.

pwc <- prince_tidy %>%
  filter(n() >= 20) %>%  #High counts
  pairwise_count(word, song, sort = TRUE) %>%
  filter(item1 %in% c("love", "peace", "gangster", "hate")) %>%
  group_by(item1) %>%
  slice(seq_len(7)) %>%
  ungroup() %>%
  mutate(row = -row_number()) #Descending order

pwc %>%
  ggplot(aes(row, n, fill = item1)) +
  geom_bar(stat = "identity", show.legend = FALSE) +
  facet_wrap(~item1, scales = "free") +
  scale_x_continuous(  #This handles replacement of row
      breaks = pwc$row, #Notice need to reuse data frame
      labels = pwc$item2) +
  theme_lyrics() + theme(panel.grid.major.x = element_blank()) +
  xlab(NULL) + ylab(NULL) +
  ggtitle("Pairwise Counts") +
  coord_flip()

comptages par paires

Comparez avec la corrélation par paires : fréquence d’apparition conjointe rapportée à la fréquence d’apparition séparée. Utilisez pairwise_cor() pour la corrélation entre mots selon leurs cooccurrences dans la même chanson.

prince_tidy %>%
  group_by(word) %>%
  filter(n() >= 20) %>%
  pairwise_cor(word, song, sort = TRUE) %>%
  filter(item1 %in% c("love", "peace", "gangster", "hate")) %>%
  group_by(item1) %>%
  top_n(7) %>%
  ungroup() %>%
  mutate(item2 = reorder(item2, correlation)) %>%
  ggplot(aes(item2, correlation, fill = item1)) +
  geom_bar(stat = 'identity', show.legend = FALSE) +
  facet_wrap(~item1, scales = 'free') +
  theme_lyrics() + theme(panel.grid.major.x = element_blank()) +
  xlab(NULL) + ylab(NULL) +
  ggtitle("Pairwise Correlation") +
  coord_flip()

corrélation par paires

Ces aperçus sont passionnants, rien qu’avec ces quatre mots ! En observant ces résultats, on commence à voir émerger des thématiques. Une parfaite transition vers le prochain tutoriel sur la modélisation de sujets.

Conclusion

Dans ce tutoriel, vous avez créé un jeu « tidy », analysé des informations de base comme la diversité lexicale et le nombre de chansons par année, puis examiné la relation entre décennie de sortie et présence au classement. Vous avez ensuite exploré des lexiques de sentiment et leur adéquation avec les paroles. Vous avez analysé le sentiment sur l’ensemble des morceaux, dans le temps, au niveau des chansons, ainsi que l’impact des bigrammes. Tout cela à l’aide d’une grande variété de graphiques, chacun apportant un angle différent.

Peut-on écrire un programme pour détecter l’humeur des paroles ? Oui. La fiabilité dépend-elle de nombreux facteurs ? Absolument : niveau de préparation des données, choix du lexique, méthode d’analyse, qualité des sources, etc. Mettre en regard les événements réels, personnels et sociétaux, éclaire l’humeur d’un texte. Chez Prince, la polarité semble décliner légèrement au fil du temps, mais dans l’ensemble, la joie ressort. Les titres classés paraissent plus positifs que les non classés. Les « prédictions » du 11 septembre et de sa propre mort semblent étrangement résonner avec ses mots.

Mais les paroles sont complexes, et trop d’hypothèses peuvent induire en erreur. En emportant ces enseignements vers la Partie Deux-B, consacrée à la modélisation de sujets, vous serez bien armé pour dégager thèmes et motifs. Jusqu’où peut-on faire du NLP sur des paroles sans ponctuation ni structure de phrase ? De quoi Prince parlait-il vraiment ? Maintenant que vous percevez l’humeur, vous êtes mieux préparé pour explorer les thématiques. Enfin, peut-on appliquer le machine learning pour prédire la décennie ou le niveau de classement d’un titre ? Retrouvez-moi dans les prochains tutoriels pour le découvrir !

J’espère que le voyage vous a plu jusqu’ici. En Partie Deux-B, on va faire la fête comme en 1999 !

Sujets
R
Science des données
Intelligence artificielle

En savoir plus sur R

{count, plural, one {Cours} other {Cours}}

Analyse de sentiments en R

{hours} h
14.1K
Apprenez l’analyse des sentiments en identifiant le langage positif et négatif, l’intention émotionnelle spécifique et en créant des visualisations percutantes.
Voir les détailsRight Arrow
Commencer Le Cours
Voir plusRight Arrow
Contenus associés

{count, plural, one {blog} other {blogs}}

Architecture de l'entrepôt de données : Tendances, outils et techniques

Apprenez l'essentiel de l'architecture d'un entrepôt de données, des composants clés aux meilleures pratiques, pour construire un système de données évolutif et efficace !
Kurtis Pykes 's photo

Kurtis Pykes

15 min

{count, plural, one {Tutoriel} other {Tutoriels}}

Régression MCO : Les idées clés expliquées

Gagnez en confiance dans la régression par les MCO en maîtrisant ses fondements théoriques. Découvrez comment réaliser des mises en œuvre simples dans Excel, R et Python.
Josef Waples's photo

Josef Waples

8 min

{count, plural, one {Tutoriel} other {Tutoriels}}

Tutoriel sur les boucles « for » en Python

Apprenez à implémenter des boucles « for » en Python pour itérer une séquence ou les lignes et colonnes d'un DataFrame pandas.
Aditya Sharma's photo

Aditya Sharma

5 min

{count, plural, one {Tutoriel} other {Tutoriels}}

Power Pivot Excel : guide pas à pas

Apprenez à relier des tables, écrire des formules DAX et construire des rapports interactifs dans Excel.
Laiba Siddiqui's photo

Laiba Siddiqui

14 min

{count, plural, one {Tutoriel} other {Tutoriels}}

Normalisation vs. Standardisation: comment faire la différence

Découvrez les principales différences, les applications et la mise en œuvre de la normalisation et de la standardisation dans le prétraitement des données pour l’apprentissage automatique.
Samuel Shaibu's photo

Samuel Shaibu

9 min

{count, plural, one {Tutoriel} other {Tutoriels}}

Tutoriel Python sur les structures de données

Initiez-vous aux structures de données de Python : apprenez-en plus sur les types de données et les structures de données primitives et non primitives, telles que les chaînes de caractères, les listes, les piles, etc.
Sejal Jaiswal's photo

Sejal Jaiswal

24 min

Voir PlusVoir Plus