Ga naar hoofdinhoud

Opgeruimde sentimentanalyse in R

Leer hoe je opgeruimde sentimentanalyse in R uitvoert op nummers van Prince, sentiment in de tijd, sentimentscores per nummer, de impact van bigrams, en nog veel meer!
· 15 min lezen

Verken met AI

Maak een sentimentele reis door het leven en de tijden van Prince, The Artist, in deel Twee-A van een driedelige tutorialserie die sentimentanalyse met R gebruikt om inzicht te krijgen in The Artist’s carrière en maatschappelijke invloed. De drie tutorials behandelen het volgende:

Wil je meer leren over sentimentanalyse? Bekijk dan zeker onze cursus Sentiment Analysis in R: The Tidy Way.

Wist je dat Prince op het podium 9/11 voorspelde, drie jaar voordat het gebeurde? Mogelijk voorspelde hij ook zijn eigen dood. Zou dat kunnen? Het zou interessant zijn om zijn songteksten te onderzoeken en zelf een weloverwogen oordeel te vellen. Op 21 april 1985 nam hij "Sometimes It Snows In April" op. Hij stierf exact 31 jaar later op 21 april 2016. Kijk naar zijn woordkeuze in de sentimentanalyse van dit nummer:

Sometimes It Snows In April
Sentimentanalyse van "Sometimes It Snows In April"

Dit geeft een nieuwe betekenis aan predictive analytics!

Structuur

Elk codefragment wordt gevolgd door een inzicht dat meestal subjectief van aard is. Voel je vrij om van sectie naar sectie te gaan en je eigen aannames te maken als je dat wilt! Ik raad je aan de volgende inleiding te lezen, maar als je meteen naar de code wilt, klik dan hier. Begrijp je het concept achter lexicons al, neem dan de snelle route rechtstreeks naar de sectie met gedetailleerde analyse.

Introductie

Drietallen: samenvatting in drie delen

Ik heb net twee kittens en een puppy gered, dus ik denk nu in drieën. Bovendien is de regel van drie een schrijfprincipe dat suggereert dat dingen die in drietallen komen inherent grappiger, bevredigender of effectiever zijn dan andere aantallen of dingen — daarom heb ik dit in drie delen opgedeeld... In Deel één maakte je kennis met textmining en verkennende analyse met een dataset van honderden songteksten van de legendarische artiest Prince. In Deel Twee focus je op sentimentanalyse (Twee-A) en topic modeling (Twee-B) met NLP. Deel Drie sluit af met aanvullende voorspellende analysetaken met machinelearningtechnieken rond vragen zoals of het mogelijk is te bepalen in welk decennium een nummer is uitgebracht of — interessanter nog — te voorspellen of een nummer de Billboard-hitlijsten haalt op basis van de songtekst. Doorheen deze tutorials gebruik je verschillende machinelearningalgoritmen — elk gemarkeerd in rood in de grafiek hieronder.

Machine Leaning Techniques
Aangepast van de grafiek die hier wordt getoond. Let op dat de verschillende aspecten van modellering en machinelearningtechnieken niet per se in één hokje passen zoals hierboven. Dit is enkel om je een idee te geven van wat je kunt verwachten.

Uit elkaar gaan is moeilijk

Ja, ik had me vastgelegd op slechts drie delen, maar ik heb besloten Deel Twee op te splitsen in Twee-A (sentimentanalyse) en Twee-B (topic modeling — volgt binnenkort), wat resulteert in vier afzonderlijke tutorials. Er zijn veel goede artikelen en cursussen over het proces achter sentimentanalyse, dus het doel in Deel Twee-A is te focussen op het afleiden van inzichten uit de analyse — niet alleen het schrijven van code. Je gebruikt de songteksten van Prince als voorbeeld, maar je kunt de stappen toepassen op je eigen favoriete artiest. Je reis door de volledige tutorialserie moet hopelijk je verwondering prikkelen over de mogelijkheden van tekstuele (lyrische) analyse, terwijl je wordt ingewijd in Natural Language Processing (NLP) en machinelearningtechnieken.

Vereisten

Deel Twee-A van deze tutorialserie vereist een basisbegrip van tidy data — specifiek pakketten zoals dplyr voor datatransformatie, ggplot2 voor visualisaties, en de %>% pipe-operator (oorspronkelijk uit het magrittr-pakket). Je ziet dat verschillende stappen vaak gecombineerd worden met de %>%-operator om de code compact en herbruikbaar te houden.

Omdat deze tutorial ook een casestudy is, zit hij vol met visualisaties die verder gaan dan de gebruikelijke grafieken en diagrammen. Daarom is nog een vereiste dat je nieuwe pakketten kunt installeren in je eigen omgeving. Om te focussen op het vastleggen van inzichten, heb ik code opgenomen die is ontworpen om hergebruikt te worden op je eigen dataset, maar voor een gedetailleerde uitleg is verder onderzoek van jouw kant nodig. Voor elk pakket worden ondersteunende links gegeven. Zo maak je kennis met nieuwe tools, terwijl je focust op hun toepassing.

Tip: lees eerst Deel één om een basis te leggen voor de tutorialserie en inzicht te krijgen in de dichte en diverse woordenschat van de songteksten van Prince.

Overzicht van sentimentanalyse

  • Methode: Sentimentanalyse is een vorm van textmining die erop gericht is de opinie en subjectiviteit van inhoud te bepalen. Toegepast op songteksten kunnen de resultaten niet alleen representatief zijn voor de houding van de artiest, maar ook wijdverspreide culturele invloeden onthullen. Er zijn verschillende methoden voor sentimentanalyse, waaronder het trainen op een bekende dataset, het opstellen van eigen classificatieregels en het gebruik van vooraf gedefinieerde lexicale woordenboeken (lexicons). In deze tutorial gebruik je de lexicongebaseerde aanpak, maar ik raad je aan de andere methoden en hun afwegingen ook te onderzoeken.

  • Niveaus: Net zoals er verschillende methoden zijn voor sentimentanalyse, zijn er ook verschillende niveaus van analyse op basis van de tekst. Deze niveaus zijn meestal document, zin en woord. Bij songteksten kan het document worden gedefinieerd als sentiment per decennium, jaar, chartniveau of nummer. Zinsniveau is bij songteksten vaak geen optie, omdat interpunctie rijm en patronen kan verstoren. Woordniveau-analyse onthult gedetailleerde informatie en kan dienen als basiskennis voor meer geavanceerde praktijken in topic modeling.

Bereid je vragen voor!

Elk data science-project moet een set vragen hebben om te onderzoeken. Houd de volgende vragen in gedachten terwijl je met deze tutorial aan de slag gaat: is het mogelijk een programma te schrijven dat de stemming in songteksten bepaalt? Zijn vooraf gedefinieerde lexicons voldoende? Hoeveel datapreparatie is nodig? Kun je je resultaten koppelen aan echte gebeurtenissen? Verandert sentiment in de tijd? Zijn hits positiever of negatiever dan nummers die niet in de hitlijsten staan? Welke woorden springen eruit in de songteksten in het jaar waarin gezegd wordt dat Prince 9/11 voorspelde? Voorspelde hij zijn eigen dood?

Tip: lyrische analyse is heel anders en meestal complexer dan toespraken of boeken, waardoor het lastig is om inzichten te vangen. Wees dus voorzichtig met alle aannames! In deze tutorial zal ik meer vragen dan antwoorden aandragen, dus wees bereid om buiten de rechthoekige parallellogram te denken!

Voorbereidend werk

Libraries en functies

Om te beginnen met het analyseren van de songteksten van Prince, laad je de onderstaande libraries. Dit lijkt misschien veel, maar de meeste zijn voor grafieken en diagrammen. Gezien het frequente gebruik van visuals is het prettig om een standaardkleurschema te definiëren voor consistentie. Ik heb een lijst gemaakt met specifieke ANSI-kleuren. De functie theme() uit ggplot2 maakt het mogelijk afzonderlijke grafieken aan te passen, dus maak je ook je eigen functie, theme_lyrics(), die de standaardinstellingen wijzigt. Het pakket knitr is een engine voor dynamische rapportgeneratie met R. Gebruik dit samen met kableExtra en formattable om aantrekkelijke tekttabellen met kleur te maken. Maak opnieuw je eigen functie, my_kable_styling(), om de output van deze libraries te standaardiseren. De andere pakketten noem ik zodra ze gebruikt worden.

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

Sushi-data

Zoals altijd begin je met het inlezen van de ruwe data in een data frame. In Deel één heb je de oorspronkelijke dataset opgeschoond, zoals het uitschrijven van samentrekkingen, het verwijderen van escape-sequenties en het omzetten van tekst naar kleine letters. Je hebt die schone dataset opgeslagen naar een CSV-bestand voor gebruik in deze tutorial. Gebruik read.csv() om prince_data te maken en houd in gedachten dat je songteksten tekenreeksen moeten zijn, dus zorg dat je stringsAsFactors = FALSE zet. Omdat je de rijnummers niet nodig hebt, zet je row.names = 1.

Let op dat je deze parameters kunt vermijden door dit in te lezen in een modern data frame, een tibble, met read_csv(), maar voor consistentie met Deel één wordt hier read.csv() gebruikt.

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

Kijk snel naar de data:

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

Je ziet dat prince_data een data frame is met 824 nummers en 10 kolommen. Dat betekent dat een record letterlijk een nummer is!

Schone pret: prince_tidy

Hier zijn nog een paar datamanipulatiestappen:

  • Verwijder ongewenste woorden (handmatige lijst met onnodige woorden)
  • Verwijder stopwoorden (zeer algemene woorden zoals "and", "the", "a", "of", enz.)
  • Verwijder woorden met minder dan drie tekens (vaak gebruikt voor fonetisch effect in muziek)
  • Splits de songteksten op in afzonderlijke woorden

Om je ruwe data in een opgeruimd formaat te zetten, gebruik je unnest_tokens() uit tidytext om prince_tidy te maken, dat de songteksten opbreekt in losse woorden met één woord per rij. Daarna kun je anti_join() en filter() uit dplyr gebruiken voor de resterende schoonmaakstappen. (Zie Deel één voor een gedetailleerde uitleg.)

#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, 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...

Je nieuwe dataset, prince_tidy, is nu in een getokeniseerd formaat met één woord per rij, samen met het nummer waaruit het komt. Je hebt nu een data frame van 76116 woorden en 10 kolommen.

Beschrijvende statistiek

Als je Deel één nog niet hebt gelezen, wil je misschien snel naar een paar samenvattende grafieken van de volledige dataset kijken. Dat doe je met creatieve grafieken uit de pakketten ggplot2, circlize en yarrr.

Opgeruimd en wel: woordenaantal per nummer

Een piraat zou shipshape zeggen wanneer alles netjes op orde is. Hier is dus een interessante weergave van de schone, opgeruimde data die de lexicale diversiteit toont, of anders gezegd: de woordenschat van de songteksten in de tijd. Een pirate plot is een geavanceerde methode om een continue afhankelijke variabele, zoals het aantal unieke woorden, uit te zetten als functie van een categorische onafhankelijke variabele, zoals decennium. Dit combineert ruwe datapunten, beschrijvende en inferentiële statistiek in één effectieve plot. Bekijk deze mooie blog voor meer details over pirateplot() uit het pakket yarrr.

Maak het data frame word_summary dat het aantal onderscheidende woorden per nummer berekent. Hoe diverser de songtekst, hoe groter de woordenschat. Zo naar de data kijken bereidt je voor op analyse op woordniveau. Zet het decenniumveld op de waarde "NONE" voor nummers zonder releasedatum en herlabel die velden met schonere labels met 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

lexical diversity per decade

Elke gekleurde cirkel in deze pirate plot staat voor een nummer. Het dichte rode gebied met de waarde "NONE" laat zien dat een groot aantal nummers in de dataset geen releasedatum heeft. Er is een lichte stijgende trend in het unieke aantal woorden per nummer in de vroege decennia: de vaste horizontale lijn toont het gemiddelde aantal woorden voor dat decennium. Dit is belangrijk om te weten als je het sentiment in de tijd gaat analyseren. De woorden worden door de loop van Prince’ carrière steeds veelzeggender.

Ik daag je uit om pirate plots verder te verkennen, want je hebt met deze grafiek nog maar het oppervlak aangeraakt!

Het hele jaar door: aantal nummers per jaar

Circulaire grafieken zijn een unieke manier om complexe (of simpele!) relaties tussen meerdere categorieën te visualiseren. (En lp’s waren rond!). De onderstaande grafiek is simpelweg een circulaire staafgrafiek met coord_polar() uit ggplot2 die het relatieve aantal nummers per jaar toont. Het is veel code voor zo’n eenvoudige grafiek, maar absoluut de moeite. Een soortgelijk voorbeeld vind je uitgebreider in de R Graph Gallery hier. Net als bij pirate plots is dit slechts een introductie tot wat je kunt doen met circulaire plots.

Aantal nummers per jaar
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)))

songs without release dates

Zie je de opening helemaal bovenaan? Dit wijst op de honderden nummers zonder releasedata die je in de pirate plot zag. Met zo’n groot aantal niet-uitgebrachte nummers zou de grafiek onbruikbaar worden als je ze zou meenemen. De ontbrekende jaren geven de jaren aan waarin geen nummers zijn uitgebracht. De meest productieve jaren waren 1996 en 1998.

Wil je weten waarom? Lees verder!

Akkoorden: nummers in de hitlijsten per decennium

De volgende grafiek laat de relatie zien tussen het decennium waarin een nummer werd uitgebracht en of het de Billboard-hitlijsten haalde. Een chordDiagram() gebruiken voor muzikale analyse leek gewoon passend! Deze grafische tool komt uit het prachtige circlize-pakket van Zuguang Gu. De grafiek is opgesplitst in twee categorieën: chartstatus (boven) en decennium (onder). De twee categorieën zijn gescheiden door brede openingen, met kleinere openingen tussen de waarden. De algemene opmerkingen in de onderstaande code kun je hier aanvullen met meer detail.

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

relationship between chart and decade

De bovenstaande cirkelgrafiek lijkt op het eerste gezicht complex, maar laat mooi de aantallen nummers per decennium per chartniveau zien. Je ziet dat Prince zijn carrière in de jaren zeventig begon met slechts enkele releases, waarvan sommige de hitlijsten haalden. Vergelijk je de jaren tachtig met de jaren negentig, dan zie je dat er in de jaren negentig meer nummers zijn uitgebracht, maar dat in de jaren tachtig meer nummers de hitlijsten haalden. Er waren slechts enkele commercieel succesvolle nummers in de jaren 2000 en in de jaren 2010 waren er geen hits.

Lexicons en lyrics

In deze sectie ga je:

  • De sentimentlexicons en hun woordenaantallen verkennen
  • Bepalen hoeveel woorden uit de songteksten in de lexicons voorkomen
  • Naar specifieke woorden en woordvormen kijken
  • Extra datapreparatie overwegen

Verken sentimentlexicons

Het pakket tidytext bevat een dataset genaamd sentiments met verschillende afzonderlijke lexicons. Deze lexicons zijn woordenboeken met woorden waaraan een sentimentcategorie of -waarde is toegekend. tidytext biedt drie lexicons voor algemeen gebruik:

  • AFINN: kent woorden een score toe tussen -5 en 5, waarbij negatieve scores negatief sentiment aangeven en positieve scores positief sentiment
  • Bing: deelt woorden in in positieve en negatieve categorieën
  • NRC: deelt woorden in in een of meer van de volgende tien categorieën: positive, negative, anger, anticipation, disgust, fear, joy, sadness, surprise en trust

Om de lexicons te bekijken, maak je een data frame new_sentiments. Filter een financieel lexicon eruit, maak voor het AFINN-lexicon een binaire (ook wel polair genoemde) sentimentkolom door de numerieke score om te zetten naar positive of negative, en voeg een veld toe met het aantal verschillende woorden per lexicon.

new_sentiments heeft één kolom met de verschillende sentimentcategorieën. Voor een beter beeld van de woordenaantallen per lexicon, per categorie, gebruik je spread() uit tidyr om die categorieën te pivoteren naar afzonderlijke velden. Profiteer van de pakketten knitr en kableExtra in de eerder gemaakte functie my_kable_styling(). Voeg kleur toe aan je tabel met color_tile() en color_bar() uit formattable om een mooi geformatteerde tabel te maken. Print je tabel en bekijk de verschillen tussen de lexicons.

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")
Woordenaantallen per lexicon
lexiconwords_in_lexiconangeranticipationdisgustfearjoynegativepositivesadnesssurprisetrust
AFINN2476NANANANANA1597879NANANA
bing 6785NANANANANA47822006NANANA
nrc 64681247839105814766893324231211915341231

De bovenstaande tabel geeft je een idee van de omvang en structuur van elk lexicon.

Match dot common

Om te bepalen welk lexicon het meest toepasbaar is op de songteksten, wil je kijken naar de matchratio van woorden die zowel in het lexicon als in de lyrics voorkomen. Ter herinnering: er zijn 76116 totale woorden en 7851 verschillende woorden in prince_tidy.

Hoeveel van die woorden staan er daadwerkelijk in de lexicons?

Gebruik een inner_join() tussen prince_tidy en new_sentiments en groepeer dan per lexicon. Het NRC-lexicon heeft 10 verschillende categorieën en een woord kan in meer dan één categorie voorkomen: woorden kunnen bijvoorbeeld negatief en verdrietig zijn. Dat betekent dat je n_distinct() in summarise() wilt gebruiken om het aantal verschillende woorden per lexicon te krijgen.

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")
Songteksten gevonden in lexicons
lexiconlex_match_wordswords_in_lyricsmatch_ratio
AFINN77078510.0980767
bing 118578510.1509362
nrc 167878510.2137307

Het NRC-lexicon bevat meer van de verschillende woorden uit de songteksten dan AFINN of Bing. Merk op dat de som van de matchratio’s laag is. Geen enkel lexicon kan alle woorden bevatten, en dat hoeft ook niet. Veel woorden zijn neutraal en hebben geen bijbehorend sentiment. Bijvoorbeeld, 2000 is doorgaans een neutraal woord en staat daarom niet in de lexicons. Maar als je je herinnert, voorspelden mensen dat vliegtuigen uit de lucht zouden vallen en computers zouden stoppen met werken in dat jaar. Er is dus een bijbehorende angst in de songtekst die niet wordt vastgelegd in sentimentanalyse met standaardlexicons.

Hier zijn enkele redenen waarom een woord niet in een lexicon voorkomt:

  • Niet elk woord draagt sentiment.
  • De lexicons zijn gemaakt voor andere soorten tekst, dus niet voor songteksten.
  • De feitelijke vorm van het woord komt mogelijk niet voor. Bijvoorbeeld, strong komt misschien voor, maar strongly niet. Er kan meer schoonmaak nodig zijn in de data! (Hier kom ik later kort op terug.)

Geloof me niet op m’n woord

Kijk naar enkele specifieke woorden uit de songteksten van Prince die waarschijnlijk impact hebben op sentiment. Staan ze in alle lexicons?

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")
Specifieke woorden
wordsentimentlexiconwords_in_lexicon
controversynegativenrc 6468
controversynegativebing 6785
dark sadnessnrc 6468
dark negativebing 6785
discouragednegativeAFINN2476
gangster negativebing 6785

Controversy en dark komen voor in NRC en Bing, maar gangster alleen in Bing. Race komt helemaal niet voor en is een cruciaal onderwerp in Prince’ songteksten. Maar is het makkelijk te koppelen aan een sentiment? Let op dat AFINN veel kleiner is en slechts één van deze woorden bevat.

Woordvormen

Nu een ingewikkelder voorbeeld. Seksualiteit is een terugkerend thema in Prince’ muziek. Hoe wordt sentimentanalyse op basis van vooraf gedefinieerde lexicons beïnvloed door verschillende vormen van een woord? Hier zijn bijvoorbeeld alle verwijzingen naar het stamwoord sex in de songteksten. Vergelijk deze met Bing en NRC en kijk waar er matches zijn.

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")
Afhankelijkheid van woordvorm
wordsentimentlexiconwords_in_lexiconinstances
sexy positivebing 6785220
sexy positiveAFINN2476220
sex anticipationnrc 6468185
sex joynrc 6468185
sex positivenrc 6468185
sex trustnrc 6468185
superfunkycalifragisexy NANA NA19
lovesexy NANA NA16
sexual NANA NA11
sexuality NANA NA11
sexiness NANA NA2
sexually NANA NA2
sexe NANA NA1
sexed NANA NA1
sexier NANA NA1
superfunkycalifraagisexyNANA NA1
superfunkycalifragisexi NANA NA1

Merk op dat Prince sexy vaak gebruikt, maar dat dit in deze vorm niet in NRC voorkomt. Het woord sex staat wel in NRC maar niet in Bing. Wat als je naar de stammen of wortels van woorden kijkt, helpt dat? Wat een dilemma! Je tekst kan een voltooid deelwoord, een meervoud of een bijwoord van een stamwoord bevatten, maar het komt mogelijk in geen enkel lexicon voor. Hoe ga je daarmee om?

Meer datapreparatie?

Het kan nodig zijn dat je nog een paar stappen in de datapreparatie uitvoert. Hier zijn drie technieken om te overwegen voordat je sentimentanalyse doet:

  • Stemming: verwijst doorgaans naar het verwijderen van suffixen van woorden om de gemeenschappelijke oorsprong te krijgen
  • Lemmatization: het reduceren van verbogen (of soms afgeleide) woorden tot hun stam, basis- of grondvorm
  • Woordvervanging: woorden vervangen door vaker gebruikte synoniemen

Een geavanceerd concept in sentimentanalyse is dat van synoniem- (semantisch vergelijkbare “peers”) en hyperoniemvervanging (een gemeenschappelijke “overkoepelende” term). Dit zijn woorden die vaker worden gebruikt dan het gerelateerde woord in de songtekst en die wél in een lexicon voorkomen, wat zo een hogere matchpercentage oplevert. Er is niet genoeg ruimte in deze tutorial om extra datapreparatie te behandelen, maar het is zeker iets om te overwegen!

Uitdaging: doe wat onderzoek naar lexicons en hoe ze worden gemaakt. Bestaat er al een dat beter geschikt is voor songteksten? Als je echt geïnteresseerd bent, overweeg dan wat er nodig is om je eigen lexicon te bouwen. Wat is het verschil tussen classifier-gebaseerde en lexicon-gebaseerde sentimentanalyse?

Gedetailleerde analyse

Nu je een basisbegrip hebt van de dataset en de lexicons, kun je die kennis toepassen door ze samen te voegen voor analyse. Dit zijn de stappen op hoofdlijnen:

  • Maak lexiconspecifieke datasets
  • Kijk naar polaire sentimenten over alle nummers
  • Onderzoek sentimentverandering in de tijd
  • Valideer je resultaten aan de hand van specifieke gebeurtenissen in Prince’ leven
  • Bestudeer sentimentscores op nummerniveau
  • Bekijk hoe woordparen het sentiment beïnvloeden

Maak sentimentdatasets

Begin met het maken van Prince-sentimentdatasets voor elk van de lexicons door een inner_join() uit te voeren op de functie get_sentiments(). Geef de naam van het lexicon bij elke aanroep door. Gebruik voor deze oefening Bing voor binaire en NRC voor categorische sentimenten. Omdat woorden in meerdere NRC-categorieën kunnen voorkomen, zoals Negative/Fear of Positive/Joy, maak je ook een subset zonder de categorieën positive en negative om later te gebruiken.

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

In the mood: overall sentiment

In de gedetailleerde analyse van de songteksten wil je de verschillende niveaus van tekst bekijken, zoals alle nummers, chartniveau, decennium en woordniveau. Begin met het plotten van de NRC-sentimentanalyse van de hele dataset.

(Voor de leuk heb ik de pakketten memery en magick gebruikt om afbeeldingen (memes) aan de grafieken toe te voegen.)

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)

prince nrc sentiment

Het lijkt erop dat NRC voor Prince’ songteksten sterk de positive kant op leunt. Maar staan alle woorden met een sentiment disgust of anger ook in de categorie negative? Het kan de moeite zijn om dat te checken.

Kijk nu naar het algemene sentiment volgens Bing. Van de 1185 verschillende woorden uit de songteksten van Prince die in het Bing-lexicon voorkomen: hoeveel zijn positief en hoeveel negatief?

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)

prince bing sentiment

Zou het zo kunnen zijn dat Bing ongeveer evenveel positive als negative sentiment laat zien in Prince’ muziek? Heft overall sentiment zichzelf op wanneer je naar een dataset kijkt die te groot is? Moeilijk te zeggen, maar bekijk het in stukken en kijk wat er gebeurt.

In de akoestiek bestaat fase-annulering waarbij de frequentie van twee instanties van dezelfde golf precies uit fase is en elkaar opheffen, bijvoorbeeld wanneer je een drum opneemt met twee microfoons en het geluid langer doet over een van de microfoons. Dit resulteert in totale stilte op die frequentie! Hoe zou dit kunnen gelden voor sentimentanalyse van grote datasets? Denk er eens over na.

Taalkundeprofessor: "In het Engels vormt een dubbele ontkenning een bevestiging. In het Russisch is een dubbele ontkenning nog steeds een ontkenning. Er is echter geen taal waarin een dubbele bevestiging een ontkenning kan vormen." Oneens zijnde student: "Ja hoor."

Harder zetten: chartniveau

Zet het volume van je analyse harder door het op te splitsen naar chartniveau met het Bing-lexicon. Maak een grafiek van het polaire sentiment per chartniveau. Gebruik spread() om de sentimenten in kolommen te splitsen en mutate() om een polarity-veld (positive - negative) en een veld percent_positive ($positive / total sentiment * 100$) te maken, voor een ander perspectief. Voeg voor de polariteitsgrafiek een yintercept toe met geom_hline(). Plot de grafieken naast elkaar met 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)

polarity and percent positive by chart level

Zegt dit dat nummers die de hitlijsten haalden doorgaans positiever zijn dan negatiever? Zo ja, wat zegt dit over wat het publiek wil horen? Kun je zulke aannames wel maken? Kijkend naar het positieve sentiment relatief aan het totale sentiment, lijken de genoteerde nummers net iets positiever dan negatief. Dat is interessant, gezien het Bing-lexicon zelf meer negatieve dan positieve woorden heeft.

Poolsmelting: So Blue

Omdat je naar sentiment vanuit een polair perspectief kijkt, wil je misschien zien of het weer verandert in de tijd (nerdhumor). Gebruik dit keer geom_smooth() met de methode loess voor een gladdere curve en nog een geom_smooth() met methode = lm voor een lineaire trendlijn.

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)

polarity and percent positive over time

Enkele extremen zijn in de tweede grafiek aangepast, maar de algemene trend van de polariteit in de tijd is in beide gevallen negatief.

Sfeerring

Je gebruikt opnieuw de kracht van de chordDiagram() om de relaties tussen NRC-sentimenten en decennia te bekijken. Merk op dat de sentimentcategorieën bovenin de ring staan en de decennia onderin.

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

relationship between mood and decade

Dit toont de aantallen woorden per NRC-categorie per decennium. Het is veel om in één kleine grafiek te verwerken, maar het geeft enorm veel informatie over relaties tussen categorieën en tijd. Deze diagrammen zijn ontzettend aanpasbaar en kunnen zo eenvoudig of informatief zijn als je wilt.

Sentiment in real time

Gegeven al die heerlijke cursussen die je op DataCamp hebt gevolgd, sta je vast te trappelen om die skills toe te passen op échte situaties. Doe dat nu door je analyse van Prince’ songteksten te mappen op iets echts gedurende een bepaalde tijd. Wees echter sceptisch en voorzichtig wanneer je een vereenvoudigde analyse toepast op zo’n complex onderwerp.

Ik heb een lijst met levensgebeurtenissen van Prince gemaakt, verzameld uit populaire bronnen zoals Rolling Stone, Biography.com, enz. Ik heb publiekelijk bekende jaren gekozen die overeenkomen met nummers met releasedata in onze dataset. Lees die events nu in uit princeEvents.csv. Gebruik vervolgens prince_bing en spread() om een polariteitsscore per jaar te maken. Join met het data frame events en maak een sentimentveld zodat je kleuren kunt vullen in je staafdiagram. Gebruik zoals altijd coord_flip() bij grote tekstlabels.

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 by events

Tip: je vindt princeEvents.csv hier.

Ik vind het fascinerend om de gebeurtenissen met het sentiment te vergelijken. Hoewel subjectief, vond ik ze sterk gecorreleerd. Toegegeven, ik heb de eventdataset zelf samengesteld. Ik had hem gemakkelijk kunnen bevooroordelen om te matchen met de songteksten. Maar ik heb wel meerdere bronnen per jaar gebruikt om het meest gerapporteerde feit te bepalen. Deze resultaten zouden je moeten motiveren om nog dieper in een woordniveau-analyse te duiken om te zien waarover Prince zong.

The Black Album: 1994 - 1996

In 1994 bracht Prince The Black Album uit in een poging zijn Afro-Amerikaanse publiek terug te winnen. Gezien het feit dat 1994 en de twee daaropvolgende jaren opvallen in de analyse, kun je kijken naar de topwoorden die matchen met het NRC-lexicon voor deze periode met geom_label_repel() uit het pakket ggrepel samen met geom_point(). Dit is een lastige grafiek, dus ik raad aan met de configuratie van de onderstaande code te spelen.

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

In 1994 zei Prince tegen Rolling Stone magazine,

"Als je een man belemmert om te dromen, wordt hij een slaaf. Daar was ik."

Hij verscheen dus in het openbaar met het woord "slave" prominent op zijn gezicht geschreven en verklaarde openlijk zijn anticorporatieve sentiment richting zijn platenlabel. Hij veranderde ook zijn naam in een symbool. Hoe goed matchen de bovenstaande woorden met deze gebeurtenissen? Zie je de woorden "slave", "emancipation", "black" en "change" in de grafiek?

(Onthoud, dit gaat niet alleen over Prince. Je kunt dit toepassen op elke tekst! Bijvoorbeeld: wat waren de meest trust-woorden die president Trump sprak in zijn eerste ambtsjaar?)

Dit keer persoonlijk: 1998

De grafiek Sentiment by Events hierboven gaf aan dat Prince tussen 1996 en 1998 trouwde, twee kinderen verloor en naar verluidt 9/11 op het podium voorspelde. (Zoek op YouTube naar zijn concert in 1998 in Nederland en luister zelf!) Gebruik dezelfde stappen als bij de vorige grafiek en bekijk de top 10 woorden in elke NRC-categorie in 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

Gezien het persoonlijke verlies en de dreigende terreurdreiging zijn woorden als "suicide", "waste", "mad", "hurt" en vele andere die niet in deze grafiek zijn opgenomen zeer kenmerkend voor de gebeurtenissen. Deze woorden zijn krachtig en geven echt inzicht in het sentiment van die periode.

Op de radar: radardiagrammen

Een andere goede manier om sentimenten over categorieën te vergelijken, is met een radardiagram, ook wel spinnenwebdiagram genoemd. Je kunt dit soort grafieken maken met het pakket radarchart. Ze zijn handig om te zien welke variabelen vergelijkbare waarden hebben of of er uitschieters zijn per variabele.

Je splitst deze analyse op in drie niveaus: jaar, chart en decennium. (Om ruimte te besparen laat ik alleen de code voor de specifieke jaren zien.)

  • Gebruik de dataset prince_nrc_sub die de sentimenten positive en negative niet bevat, zodat de andere duidelijker zichtbaar zijn. Bereken dit keer eerst het totale aantal woorden per sentiment per jaar, evenals het totale sentiment voor het hele jaar en verkrijg een percentage ($aantal sentimentwoorden per jaar / totaal per jaar * 100$).
  • Filter op de specifieke jaren 1978, 1994, 1995 en verwijder de onnodige velden met select().
  • Ten slotte moet je de jaar- en percentagewaarden (key/value-paren) spread()-en naar meerdere kolommen, zodat je één rij per sentiment hebt en een kolom per jaar. Gebruik dan chartJSRadar() om een interactieve HTML-widget te genereren. Je kunt een argument meegeven om datasetlabels in de mouse-over te tonen. (FYI, soms worden de J en Y van het woord "joy" door radarchart afgesneden en lijkt het op "iov".)
#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")

Je moet even scrollen om deze grafieken te vergelijken omdat ik ze op volledige grootte wilde laten. Interessant is dat je met een kleinere dataset zoals jaar de variatie in elk sentiment veel duidelijker ziet met dit type visualisatie. Je valt mogelijk ook op dat, in tegenstelling tot eerdere oefeningen, bij gebruik van percentages "joy" de hoogste score heeft op alle grafieken (vooral aan het begin van Prince’ carrière in 1978).

Sign O' the Times

Spotify gebruikt machine learning om aanbevelingen te doen voor zijn 140 miljoen actieve gebruikers. Het gebruikt songteksten nog niet. Stel je voor dat het dat wel zou doen... Neem nu een nog diepere kijk in de sfeer van specifieke nummers.

In 1987 schreef Prince een nummer genaamd "Sign O' the Times". Een Billboard-artikel van Kenneth Partridge stelde:

"Terwijl hij de onderwerpen AIDS, bendes, drugs, natuurrampen en zelfs de Challenger-explosie aanpakt, blijft Prince cool en afstandelijk. 'Sign O' the Times' is een statusupdate, geen oproep tot actie. Zijn oplossing is niet demonstreren op straat of je congreslid bellen."

Hoe zou een machine de sfeer van dit nummer interpreteren? Is het sterk emotioneel of simpelweg informatief? Vertegenwoordigt het Prince als persoon of de samenleving in het algemeen? Plot de NRC-categorieën met 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

Hoewel opnieuw sterk subjectief, lijken je resultaten te bevestigen dat Prince "cool en afstandelijk" blijft, zoals Partridge stelt. Dat kun je afleiden uit de observatie dat anticipation-woorden vaker voorkomen dan emotionele categorieën als sadness, fear en anger. Hier is een artikel dat stelt dat deze drie categorieën emoties zijn, terwijl anticipation een sentiment is. De Billboard-auteur stelde ook dat het nummer simpelweg een "statusupdate, bijna zonder emotie" is. Het lijkt erop dat de machine het daarmee eens is. Interpreteer jij het ook zo? Ik weet dat dit een R-tutorial is, maar sentimentanalyse is niet puur technisch. Men zegt dat het de plek is waar AI psychologie ontmoet.

Gebruik ggplot2 om in een iets andere grafiek naar de woorden per categorie te kijken.

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

Kun je deze woorden koppelen aan de onderwerpen in het Billboard-artikel? De connectie is vrij duidelijk: Challenger-ramp -> "rocket", AIDS -> "disease", drugs -> "crack", natuurrampen -> "hurricane", bendes -> "gang", enz.

Het is bijna alsof Partridge eerst wat R-code schreef en deze grafiek maakte voordat hij het artikel schreef!

Meer nummers

Bekijk de sentimentcategorieën voor nog een paar nummers met sprekende titels en kijk of ze gecorreleerd lijken.

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

nrc sentiment song analysis

NRC-sentimenten tonen hoge anticipation en fear voor een nummer over the future, en hetzelfde plus hoge trust voor een nummer over controversy. De nummers die overwegend sad zijn, lijken eveneens bij hun titels te passen.

Bigrams per decennium

Tot nu toe heb je alleen gekeken naar unigrams of losse woorden. Maar als "love" een veelvoorkomend woord is, wat gaat eraan vooraf? Of volgt erop? Losse woorden uit hun context bekijken kan misleidend zijn. Tijd dus om naar bigrams of woordparen te kijken.

Handig genoeg biedt het pakket tidytext de mogelijkheid om paren van woorden te unnesten, evenals losse woorden. In dit geval roep je unnest_tokens() aan met het argument token op ngrams. Omdat je alleen naar bigrams (twee opeenvolgende woorden) kijkt, geef je n = 2 door. Gebruik prince_bigrams om de resultaten op te slaan.

Het pakket tidyr biedt de mogelijkheid om bigrams op te splitsen in individuele woorden met de functie separate(). Om de stopwoorden en ongewenste woorden te verwijderen, wil je de bigrams opsplitsen en filteren wat je niet wilt, en vervolgens met unite() de woordparen weer samenvoegen. Zo kun je eenvoudig de meest voorkomende bigrams per decennium visualiseren. (Zie Deel één voor een uitleg van slice() en 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()

bigrams per decade

Met bigrams zie je bijna hoe de veelgebruikte frasen verschuiven van sex, dance en romance naar religie en (rainbow) children. Voor het geval je het niet wist: de term "rainbow baby" wordt soms gebruikt door ouders die een volgend kind verwachten na het verlies van een baby door een miskraam. Opmerkelijk genoeg kon ik geen echte recensie van Prince’ album Rainbow Children vinden die hier melding van maakt.

Sentiment met bigrams

Hoe beïnvloeden bigrams het sentiment? Gebruik dit keer het AFINN-lexicon voor sentimentanalyse op woordparen en kijk hoe vaak sentimentgebonden woorden worden voorafgegaan door "not" of andere ontkennende woorden.

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

polar sentiment of words preceded by not

Op de eerste regel van de grafiek krijgt care een vals positief sentiment omdat de "not" wordt genegeerd bij analyse van losse woorden. Heffen de vals-positieve bigrams de vals-negatieve bigrams op?

Weer een vraag die ik niet kan beantwoorden, maar wel een goed onderwerp om verder te verkennen. Tip: je kunt ook de parameters "trigrams" en "n = 3" meegeven aan unnest_tokens() om naar nog meer opeenvolgende woorden te kijken!

Er zijn ook andere ontkenningswoorden om te overwegen. Dit keer maak je een netwerkgrafiek met de pakketten ggraph en igraph. Je zet de woorden op in verbonden knopen met de ontkenningswoorden in het midden. Maak het eerste object uit de opgeruimde dataset met graph_from_data_frame() en gebruik vervolgens ggraph() om het te plotten. Je kunt de hoofdknopen markeren met een aanroep van geom_edge_density(). Meer details van een soortgelijk voorbeeld vind je in het boek van Julia Silge en David Robinson over Tidy Text Mining.

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

negation bigram network

Hier zie je de woordparen die samenhangen met ontkenningswoorden. Als je analyse is gebaseerd op unigrams en "alone" komt negatief terug, zal het bigram "not alone", zoals je hierboven ziet, het omgekeerde effect hebben. Sommige woorden kruisen meerdere knopen, wat in een visual als deze goed te zien is: bijvoorbeeld "never hurt" en "not hurt".

Pairwise-vergelijkingen

Nu je naar n-grams hebt gekeken, bekijk je de correlatie tussen woorden. Welke woorden zijn het sterkst gecorreleerd? Gebruik de functie pairwise_count() uit het pakket widyr om co-occurrence-tellingen te identificeren. Dat wil zeggen: je telt hoe vaak elk woordpaar samen voorkomt binnen een nummer. Het pakket widyr neemt een opgeruimde dataset en verbreedt die tijdelijk voordat het deze weer terugbrengt naar een opgeruimde structuur voor visualisatie en verdere analyse.

Om het simpel te houden, heb ik vier interessante woorden uit Prince’ songteksten gekozen.

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

pairwise counts

Vergelijk dat met pairwise correlation. Dit verwijst naar hoe vaak woorden samen voorkomen relatief aan hoe vaak ze afzonderlijk voorkomen. Gebruik pairwise_cor() om de correlatie tussen woorden te bepalen op basis van hoe vaak ze in hetzelfde nummer voorkomen.

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

pairwise correlation

Ik vind dit fascinerende inzichten in de songteksten, alleen al op basis van deze vier woorden! Als je naar deze resultaten kijkt, beginnen er thema’s naar voren te komen. Dit vormt een mooie opstap naar de volgende tutorial over topic modeling!

Conclusie

In deze tutorial heb je een opgeruimde dataset gemaakt en basisinformatie geanalyseerd zoals de lexicale diversiteit en het aantal nummers per uitgiftejaar, en je bekeek de relatie tussen uitgifte-decennium en of een nummer de hitlijsten haalde. Je hebt vervolgens enkele sentimentlexicons verkend en gekeken hoe goed ze matchten met de songteksten. Daarna heb je sentimentanalyse uitgevoerd over alle nummers in de dataset, sentiment in de tijd, sentimentscores per nummer en de impact van bigrams. Dit deed je met een brede variatie aan interessante grafieken, die elk een ander perspectief geven.

Is het dus mogelijk een programma te schrijven om stemming in songteksten te bepalen? Jazeker! Hoe betrouwbaar zijn je resultaten? Dat hangt af van een breed scala aan criteria, zoals de hoeveelheid datapreparatie, de keuze van het lexicon, de analysemethode, de kwaliteit van de brondata, enzovoort. Het vergelijken met echte gebeurtenissen, zowel persoonlijk als maatschappelijk, kan de stemming van elke songtekst verduidelijken. Prince’ polaire sentiment leek in de tijd licht te dalen, maar overall lijkt joy eruit te springen. Nummers die de hitlijsten halen lijken positiever dan nummers die dat niet doen. De claims dat hij 9/11 en zijn eigen dood voorspelde, lijken griezelig goed te rijmen met zijn woorden.

Maar songteksten zijn complex en te veel aannames kunnen problemen veroorzaken. Als je meeneemt wat je over de songteksten hebt geleerd naar de volgende tutorial, Deel Twee-B over topic modeling, ben je goed op weg om thema’s en motieven uit te graven. Hoeveel NLP kun je toepassen op songteksten die zonder interpunctie of zinsstructuur verschijnen? Waar zong Prince nu echt over? Nu je de stemming begrijpt, ben je beter voorbereid om de mogelijke topics te onderzoeken. En tot slot: kun je machinelearningtechnieken toepassen om het decennium of het chartniveau van een nummer te voorspellen? Ga mee naar de volgende tutorials om het te ontdekken!

Ik hoop dat je de reis tot nu toe leuk vond. In Deel Twee-B gaan we feesten alsof het 1999 is!

Onderwerpen

Gerelateerd