In this post, we'll return to the Kaggle data containing information on Pitchfork music reviews. In a previous post, I used this dataset to cluster music genres. In the current post, I will use R and the tidytext package (and philosophy) to examine the text of the music reviews. Specifically, the goal of the analysis described in this post will be to track the course of positive and negative sentiment use across the length of the review texts.The DataThe data are available on the Kaggle website. I did an extensive data munging exercise to clean and prepare the data for analysis. As noted in the previous post using these data, some of the music reviews are assigned multiple genres. As we will later be interested in examining sentiment use in the different genres, I excluded reviews with more than 1 genre from the scope of this analysis, leaving us with 12,147 review texts. In this post we will focus on 3 columns in our dataset: one column containing a unique review identifier, one column with the review text, and one column that contains the genre of the album being reviewed (with the following options: electronic, experimental, folk/country, global, jazz, metal, pop/rnb, rap and rock).The tidytext ApproachIn order to prepare our data for analysis, we must turn it into a tidy dataset. The basic idea behind the tidytext framework is that we represent our data with 1 line per token (a sub-division of a longer text, typically but not always a single word), keeping track of important meta-data (e.g. the id number of the review the word appears in) in additional columns.I was first introduced to this way of thinking about text analysis at Julia Silge's excellent talk at the useR conference in Brussels last June. I have done lots of text analysis (with R and Python), and I have always used the &quot;bag of words&quot; framework, in which each text is kept in a single line of the dataset. This traditional approach is very handy when doing, for example, predictive analysis with text. However, the tidytext philosophy lets us think about and analyze our data in a slightly different way. Check out the excellent tidytext book, available freely online, for a thorough overview of the approach and its implementation in R.One of the advantages of the tidytext approach is that it retains information about word order that is lost when using the bag of words approach. A nice illustration of using word order in quantitative text analysis is described in the tidytext book and vignette. This analysis examines the balance of emotion words across the course of each of Jane Austen's novels. I was inspired by this approach, and the current post is an adaptation of this idea, applied to Pitchfork music reviews.Data Preparation The head of our raw dataset (called text_df), which serves as the input for our analysis, looks like this:*linetextgenre11“Trip-hop” eventually became a ’90s punchline, a music-press shorthand for “overhyped hotel lounge music.”... electronic22Eight years, five albums, and two EPs in, the New York-based outfit Krallice have long since shut up purists about their “hipster black metal.” ...metal33Minneapolis’ Uranium Club seem to revel in being aggressively obtuse... rock44Kleenex began with a crash. It transpired one night not long after they’d formed, in Zurich of 1978... rock55It is impossible to consider a given release by a footwork artist without confronting the long shadow cast by DJ Rashad’s catalog...electronic66Rapper Simbi Ajikawo, who records as Little Simz, is by all measures on an upward trajectory...rapThe column &quot;line&quot; serves as an indicator of the review id. The column &quot;text&quot; contains the text of the review and the &quot;genre&quot; column indicates the genre.We will first turn our raw data into a tidy text dataframe (as explained in the tidytext vignette):# load the packages we'll be usinglibrary(plyr); library(dplyr)library(tidytext)library(tidyr)library(ggplot2)# unnest to one line # https://cran.r-project.org/web/packages/tidytext/vignettes/tidytext.htmltidy_reviews % unnest_tokens(word, text)Our data have now been transformed into a tidy format (only first 10 rows shown):linegenreword11electronictrip21electronichop31electroniceventually41electronicbecame51electronica61electronic90s71electronicpunchline81electronica91electronicmusic101electronicpressAs shown above, our data now contains one word per line, and our meta-data (review id and genre) are contained in two additional columns. Note that, by default, the unnest_tokens function removes punctuation and converts all letters to lower case.Unnesting our text data gives us a narrow but extremely long dataframe. Specifically, our dataframe contains as many rows as there are words in the 12,147 reviews: 8,182,882 to be precise!As we are interested in understanding the course of emotional valence throughout the texts, we will add a column indicating the order of the words within each review, which I will call &quot;position_in_review_0.&quot;# add position within review texttidy_reviews % group_by(line) %__% mutate(position_in_review_0 = 1:n())Our dataset, called tidy_reviews, now looks like this (only first 10 rows shown):linegenrewordposition_in_review_011electronictrip121electronichop231electroniceventually341electronicbecame451electronica561electronic90s671electronicpunchline781electronica891electronicmusic9101electronicpress10We can see that the data contain stopwords (words which occur frequently but contain no meaningful content such as &quot;the&quot;, &quot;a&quot; etc.). Before we continue, let's remove these stopwords. In the tidytext approach, this is done with an anti-join of our tidy text dataframe against a tidy dataframe containing a list of stopwords. We then order the dataset by the review id (called line) and by the word order position (position_in_review_0) in the raw data. We create a new word order variable, called position_in_review, which gives the position of each word in the cleaned data (without stopwords), while removing the position_in_review_0 variable created above.# remove stop words# order the dataset by review id# and the order of the remaining words# in the original datasetcleaned_reviews % anti_join(stop_words) %__% arrange(line, position_in_review_0)# add position of word within cleaned review# we also remove the first word order column# (position_in_review_0) created abovecleaned_reviews % group_by(line) %__% mutate(position_in_review = 1:n()) %__% select(-position_in_review_0)The data (called cleaned_reviews) now look like this (only first 10 rows shown):linegenrewordposition_in_review11electronictrip121electronichop231electroniceventually341electronic90s451electronicpunchline561electronicmusic671electronicpress781electronicshorthand891electronicoverhyped9101electronichotel10Our goal was to preserve the word order so that we can track the use of emotion words across the course of the review texts. The steps necessary to achieve this were somewhat involved, but we have reached our goal. We have retained the important words in the texts and, for each word, we have created a record of its position in the review.As the reviews have differing numbers of words, we cannot simply compare the evolution of sentiment use across word number. Therefore, we will count the number of words in each review, and for each word, calculate its position in terms of its percentage in the words of the text. This will result in 101 different levels representing word position for each text (because we go from 0 to 1 in increments of .01). We will eventually aggregate the data to this level, making it possible to visualize the use of emotion words across the different percentages of the review texts.# count the number of remaining words for each review# and calculate the percentage within each review# that each word falls incleaned_reviews % group_by(line) %__% mutate(wordcount_review = n(), percentage_in_review = round(position_in_review/wordcount_review,2))Our data now look like this (only first 10 rows shown):linegenrewordposition_in_reviewwordcount_reviewpercentage_in_review11electronictrip1746021electronichop2746031electroniceventually3746041electronic90s47460.0151electronicpunchline57460.0161electronicmusic67460.0171electronicpress77460.0181electronicshorthand87460.0191electronicoverhyped97460.01101electronichotel107460.01Intermezzo: Coding Sentiment With Dictionaries There are a number of different ways of analyzing sentiment in text. One common approach is to use dictionaries, which contain pre-defined lists of words which are categorized as belonging to a particular type of higher-level characteristic we wish to understand (e.g. in the case of sentiment - positive, negative, or more fine-grained such as excitement, anxiety, etc.). In this post, we will use a dictionary set included in the tidytext package and which is described in the tidytext book and vignette. Specifically, we will use the positive and negative sentiment words contained in the &quot;bing&quot; dictionaries. For more information about the bing dictionaries, you can check out these excellent blog posts, consult the tidytext book or vignette, or type ?sentiments in the R console (with the tidytext package loaded). One thing to keep in mind is that the bing dictionaries contain many more negative words than positive words:# count of positive/negative sentiment# in bing dictionariesget_sentiments(&quot;bing&quot;) %__% count(sentiment) Which returns:  sentimentn1negative47822positive2006Indeed, there are more than twice as many negative than positive words in the bing dictionaries.Let's look at the most frequent positive and negative words from the bing dictionaries in our review data. We can make a word cloud with code directly adapted from the tidytext vignette to do this:# plot most frequent positive/negative words# with wordcloudlibrary(reshape2)library(wordcloud)cleaned_reviews %__% inner_join(get_sentiments(&quot;bing&quot;), by = &quot;word&quot;) %__% count(word, sentiment, sort = TRUE) %__% acast(word ~ sentiment, value.var = &quot;n&quot;, fill = 0, fun.aggregate = length) %__% comparison.cloud(colors = c(&quot;#F8766D&quot;, &quot;#00BFC4&quot;),max.words = 150, title.size = 1, scale=c(3.5,1))Which gives us the following plot:The vast majority of the words make sense to me, and seem to capture positive and negative things one could say about music in the context of an album review. It is amusing to see that &quot;punk&quot; is classified as negative, which makes sense in many contexts but is a bit off here. This is the downside of using general-purpose text classification dictionaries; overall they can perform quite well but they are by design not adapted for the specifics of every corpus. Despite these small imperfections, the above visualization makes clear that the bing dictionaries are picking up on meaningful indicators of sentiment in the Pitchfork reviews.Note that, in the analysis below, I will treat positive and negative sentiment separately. The analysis presented in the tidytext vignette analyzes an overall sentiment score (e.g. sentiment = positive - negative). However, this seems strange to me for 2 reasons. First, as we saw above, there are twice as many negative (vs. positive) words in the bing corpus. Second, there is a large literature in psychology that suggests that positive and negative affect are orthogonal (independent); this logic underpins the measurement of affect in the widely used PANAS questionnaire, for example.  Let's then apply these dictionaries to our data, in order to extract both positive and negative sentiment at each percentage of our review texts.Finishing the Munging We will conduct all of the necessary steps in a single dplyr chain. First, we merge in the sentiment dictionaries, retaining only the words classified as positive or negative. We then count the number of positive and negative words at each percentage in the reviews; this counting is done separately for each percentage of each review. We then aggregate the data by genre and percentage of the review text. Specifically, for each genre, we calculate the average number of positive and negative words that occur at each percentage in the review text.pitchfork_sentiment %# code the words according to the positive/negative bing sentiment dictionaries inner_join(get_sentiments(&quot;bing&quot;), by = &quot;word&quot;) %__%# count the number of pos/neg words at each percentage of each review count(genre, index = percentage_in_review, sentiment) %__%# put the pos/neg counts into their own columns spread(sentiment, n, fill = 0) %__%# for each genre, compute the average number of pos/neg words# used at each percentage of the review texts group_by(genre, index) %__% summarize(mean_negative = round(mean(negative, na.rm = TRUE),2),mean_positive = round(mean(positive, na.rm = TRUE),2))The resulting dataset, called pitchfork_sentiment, looks like this (only first 10 rows shown):genreindexmean_negativemean_positive1electronic00.670.352electronic0.010.610.543electronic0.020.610.594electronic0.030.620.545electronic0.040.640.536electronic0.050.560.597electronic0.060.620.558electronic0.070.640.569electronic0.080.630.5610electronic0.090.580.58Our data contains 9 genres, with 101 rows per genre (because we go from 0 to 1 in increments of .01), resulting in 909 total rows.Visualizing Emotional Valence Across the Album Reviews  We will produce our plots using the excellent ggplot2 package, which also is built according to the tidy data philosophy. From the tidy perspective, our data are problematic in that the values we want to visualize (mean_positive and mean_negative) are contained in two different columns. Therefore, in order to plot using ggplot2, we must transform our data from a wide to a long format, putting our observations in a single column, with an additional column containing the sentiment type (positive or negative).We can achieve this using code taken directly from the gather help page from the tidyr package (check out this webpage or type ?gather with the tidyr package loaded):# make the wide to long data# (from the &quot;gather&quot; help page in tidyr)long_sentiment_by_genre