<p><div><div><div><div data-width="100" data-show-faces="false" data-send="true" data-layout="button_count" data-height="21" data-href="http://www.r-bloggers.com/does-sentiment-analysis-work-a-tidy-analysis-of-yelp-reviews/"></div></div><div><a data-url="http://www.r-bloggers.com/does-sentiment-analysis-work-a-tidy-analysis-of-yelp-reviews/" data-counturl="http://www.r-bloggers.com/does-sentiment-analysis-work-a-tidy-analysis-of-yelp-reviews/" data-count="horizontal" data-text="Does sentiment analysis work? A tidy analysis of Yelp reviews" class="twitter-share-button" data-via="rbloggers" href="https://twitter.com/share"></a></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="http://varianceexplained.org/r/yelp-sentiment/"> Variance Explained</a></strong>, and kindly contributed to <a rel="nofollow" href="http://www.r-bloggers.com/">R-bloggers)</a> </div>
<p>This year <a target="_blank" href="http://juliasilge.com/">Julia Silge</a> and I released the <a target="_blank" href="https://github.com/juliasilge/tidytext">tidytext</a> package for text mining using tidy tools such as <a target="_blank" href="https://cran.r-project.org/package=dplyr">dplyr</a>, <a target="_blank" href="https://cran.r-project.org/package=tidyr">tidyr</a>, <a target="_blank" href="https://cran.r-project.org/package=ggplot2">ggplot2</a> and <a target="_blank" href="https://cran.r-project.org/package=broom">broom</a>. One of the canonical examples of tidy text mining this package makes possible is <a target="_blank" href="https://en.wikipedia.org/wiki/Sentiment_analysis">sentiment analysis</a>.</p>
<p>Sentiment analysis is often used by companies to quantify general social media opinion (for example, using tweets about several brands to compare customer satisfaction). One of the simplest and most common sentiment analysis methods is to classify words as “positive” or “negative”, then to average the values of each word to categorize the entire document. (See <a target="_blank" href="https://cran.r-project.org/web/packages/tidytext/vignettes/tidytext.html">this vignette</a> and <a target="_blank" href="http://juliasilge.com/blog/Life-Changing-Magic/">Julia’s post</a> for examples of a tidy application of sentiment analysis). But does this method actually <em>work</em>? Can you predict the positivity or negativity of someone’s writing by counting words?</p>
<p>To answer this, let’s try sentiment analysis on a text dataset where we know the “right answer”- one where each customer also quantified their opinion. In particular, we’ll use the <a target="_blank" href="https://www.yelp.com/dataset_challenge">Yelp Dataset</a>: a wonderful collection of millions of restaurant reviews, each accompanied by a 1-5 star rating. We’ll try out a specific sentiment analysis method, and see the extent to which we can predict a customer’s rating based on their written opinion. In the process we’ll get a sense of the strengths and weaknesses of sentiment analysis, and explore another example of tidy text mining with tidytext, dplyr, and ggplot2.</p>
<h3>Setup</h3>
<p>I’ve downloaded the <code>yelp_dataset_challenge_academic_dataset</code> folder from <a target="_blank" href="https://www.yelp.com/dataset_challenge">here</a>.<sup><a target="_blank" href="http://varianceexplained.org/r/yelp-sentiment/#fn:termsofuse">1</a></sup> First I read and process them into a data frame:</p>
<figure>
<pre><code data-lang="r"><span>library</span><span>(</span><span>stringr</span><span>)</span><span>
</span><span>library</span><span>(</span><span>jsonlite</span><span>)</span><span>
</span><span># Each line is a JSON object- the fastest way to process is to combine into a
# single JSON string and use fromJSON and flatten
</span><span>reviews_combined</span><span> </span><span>&lt;-</span><span> </span><span>str_c</span><span>(</span><span>&quot;[&quot;</span><span>,</span><span> </span><span>str_c</span><span>(</span><span>review_lines</span><span>,</span><span> </span><span>collapse</span><span> </span><span>=</span><span> </span><span>&quot;, &quot;</span><span>),</span><span> </span><span>&quot;]&quot;</span><span>)</span><span>
</span><span>reviews</span><span> </span><span>&lt;-</span><span> </span><span>fromJSON</span><span>(</span><span>reviews_combined</span><span>)</span><span> </span><span>%>%</span><span>
</span><span>flatten</span><span>()</span><span> </span><span>%>%</span><span>
</span><span>tbl_df</span><span>()</span></code></pre>
</figure>
<p>We now have a data frame with one row per review:</p>
<figure>
<pre><code data-lang="r"><span>reviews</span></code></pre>
</figure>
<figure>
<pre><code data-lang="text">## # A tibble: 200,000 x 10
##                   user_id              review_id stars       date
##                     &lt;chr>                  &lt;chr> &lt;int>      &lt;chr>
## 1  PUFPaY9KxDAcGqfsorJp3Q Ya85v4eqdd6k9Od8HbQjyA     4 2012-08-01
## 2  Iu6AxdBYGR4A0wspR9BYHA KPvLNJ21_4wbYNctrOwWdQ     5 2014-02-13
## 3  auESFwWvW42h6alXgFxAXQ fFSoGV46Yxuwbr3fHNuZig     5 2015-10-31
## 4  uK8tzraOp4M5u3uYrqIBXg Di3exaUCFNw1V4kSNW5pgA     5 2013-11-08
## 5  I_47G-R2_egp7ME5u_ltew 0Lua2-PbqEQMjD9r89-asw     3 2014-03-29
## 6  PP_xoMSYlGr2pb67BbqBdA 7N9j5YbBHBW6qguE5DAeyA     1 2014-10-29
## 7  JPPhyFE-UE453zA6K0TVgw mjCJR33jvUNt41iJCxDU_g     4 2014-11-28
## 8  2d5HeDvZTDUNVog_WuUpSg Ieh3kfZ-5J9pLju4JiQDvQ     5 2014-02-27
## 9  BShxMIUwaJS378xcrz4Nmg PU28OoBSHpZLkYGCmNxlmg     5 2015-06-16
## 10 fhNxoMwwTipzjO8A9LFe8Q XsA6AojkWjOHA4FmuAb8XQ     3 2012-08-19
## # ... with 199,990 more rows, and 6 more variables: text &lt;chr>,
## #   type &lt;chr>, business_id &lt;chr>, votes.funny &lt;int>, votes.useful &lt;int>,
## #   votes.cool &lt;int></code></pre>
</figure>
<p>Notice the <code>stars</code> column with the star rating the user gave, as well as the text column (too large to display) with the actual text of the review. For now, we’ll focus on whether we can predict the star rating based on the text.</p>
<h3>Tidy sentiment analysis</h3>
<p>Right now, there is one row for each review. To analyze in the <a target="_blank" href="http://github.com/juliasilge/tidytext">tidy text</a> framework, we need to use the <code>unnest_tokens</code> function and turn this into one-row-per-term-per-document:</p>
<figure>
<pre><code data-lang="r"><span>library</span><span>(</span><span>tidytext</span><span>)</span><span>
</span><span>review_words</span><span> </span><span>&lt;-</span><span> </span><span>reviews</span><span> </span><span>%>%</span><span>
</span><span>select</span><span>(</span><span>review_id</span><span>,</span><span> </span><span>business_id</span><span>,</span><span> </span><span>stars</span><span>,</span><span> </span><span>text</span><span>)</span><span> </span><span>%>%</span><span>
</span><span>unnest_tokens</span><span>(</span><span>word</span><span>,</span><span> </span><span>text</span><span>)</span><span> </span><span>%>%</span><span>
</span><span>filter</span><span>(</span><span>!</span><span>word</span><span> </span><span>%in%</span><span> </span><span>stop_words</span><span>$</span><span>word</span><span>,</span><span>
</span><span>str_detect</span><span>(</span><span>word</span><span>,</span><span> </span><span>&quot;^[a-z']+$&quot;</span><span>))</span><span>
</span><span>review_words</span></code></pre>
</figure>
<figure>
<pre><code data-lang="text">## # A tibble: 7,688,667 x 4
##                 review_id            business_id stars        word
##                     &lt;chr>                  &lt;chr> &lt;int>       &lt;chr>
## 1  Ya85v4eqdd6k9Od8HbQjyA 5UmKMjUEUNdYWqANhGckJw     4      hoagie
## 2  Ya85v4eqdd6k9Od8HbQjyA 5UmKMjUEUNdYWqANhGckJw     4 institution
## 3  Ya85v4eqdd6k9Od8HbQjyA 5UmKMjUEUNdYWqANhGckJw     4     walking
## 4  Ya85v4eqdd6k9Od8HbQjyA 5UmKMjUEUNdYWqANhGckJw     4   throwback
## 5  Ya85v4eqdd6k9Od8HbQjyA 5UmKMjUEUNdYWqANhGckJw     4         ago
## 6  Ya85v4eqdd6k9Od8HbQjyA 5UmKMjUEUNdYWqANhGckJw     4   fashioned
## 7  Ya85v4eqdd6k9Od8HbQjyA 5UmKMjUEUNdYWqANhGckJw     4        menu
## 8  Ya85v4eqdd6k9Od8HbQjyA 5UmKMjUEUNdYWqANhGckJw     4       board
## 9  Ya85v4eqdd6k9Od8HbQjyA 5UmKMjUEUNdYWqANhGckJw     4      booths
## 10 Ya85v4eqdd6k9Od8HbQjyA 5UmKMjUEUNdYWqANhGckJw     4   selection
## # ... with 7,688,657 more rows</code></pre>
</figure>
<p>Notice that there is now one-row-per-term-per-document: the tidy text form. In this cleaning process we’ve also removed “stopwords” (such as “I”, “the”, “and”, etc), and removing things things that are formatting (e.g. “—-“) rather than a word.</p>
<p>Now let’s perform sentiment analysis on each review. We’ll use the <a target="_blank" href="http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6010">AFINN</a> lexicon, which provides a positivity score for each word, from -5 (most negative) to 5 (most positive). This, along with several other lexicons, are stored in the <code>sentiments</code> table that comes with tidytext. (I’ve tried some other lexicons on this dataset and the results are pretty similar.)</p>
<figure>
<pre><code data-lang="r"><span>AFINN</span><span> </span><span>&lt;-</span><span> </span><span>sentiments</span><span> </span><span>%>%</span><span>
</span><span>filter</span><span>(</span><span>lexicon</span><span> </span><span>==</span><span> </span><span>&quot;AFINN&quot;</span><span>)</span><span> </span><span>%>%</span><span>
</span><span>select</span><span>(</span><span>word</span><span>,</span><span> </span><span>afinn_score</span><span> </span><span>=</span><span> </span><span>score</span><span>)</span><span>
</span><span>AFINN</span></code></pre>
</figure>
<figure>
<pre><code data-lang="text">## # A tibble: 2,476 x 2
##          word afinn_score
##         &lt;chr>       &lt;int>
## 1     abandon          -2
## 2   abandoned          -2
## 3    abandons          -2
## 4    abducted          -2
## 5   abduction          -2
## 6  abductions          -2
## 7       abhor          -3
## 8    abhorred          -3
## 9   abhorrent          -3
## 10     abhors          -3
## # ... with 2,466 more rows</code></pre>
</figure>
<p>Now as described <a target="_blank" href="http://juliasilge.com/blog/Life-Changing-Magic/">in Julia’s post</a>, our sentiment analysis is just an inner-join operation followed by a summary:</p>
<figure>
<pre><code data-lang="r"><span>reviews_sentiment</span><span> </span><span>&lt;-</span><span> </span><span>review_words</span><span> </span><span>%>%</span><span>
</span><span>inner_join</span><span>(</span><span>AFINN</span><span>,</span><span> </span><span>by</span><span> </span><span>=</span><span> </span><span>&quot;word&quot;</span><span>)</span><span> </span><span>%>%</span><span>
</span><span>group_by</span><span>(</span><span>review_id</span><span>,</span><span> </span><span>stars</span><span>)</span><span> </span><span>%>%</span><span>
</span><span>summarize</span><span>(</span><span>sentiment</span><span> </span><span>=</span><span> </span><span>mean</span><span>(</span><span>afinn_score</span><span>))</span><span>
</span><span>reviews_sentiment</span></code></pre>
</figure>
<figure>
<pre><code data-lang="text">## Source: local data frame [187,688 x 3]
## Groups: review_id [?]
## 
##                 review_id stars sentiment
##                     (chr) (int)     (dbl)
## 1  __-r0eC3hZlaejvuliC8zQ     5 4.0000000
## 2  __1yzxN39QzdeJqicAg99A     3 1.3333333
## 3  __3Vy9VLHV5jKjgFDRWCiQ     2 1.3333333
## 4  __56FUEaW57kZEm56OZk7w     5 0.8333333
## 5  __5webDfFxADKz_3k5YipA     5 2.2222222
## 6  __6QkPtePef4_oW6A_tbOg     4 2.0000000
## 7  __6tOxx2VcvGR02d2ILkuw     5 1.7500000
## 8  __77nP3Nf1wsGz5HPs2hdw     5 1.6000000
## 9  __7MkcofSZYHj9v5KuLVvQ     4 1.8333333
## 10 __7RBFUZgxef8gZ8guaVhg     5 2.4000000
## ..                    ...   ...       ...</code></pre>
</figure>
<p>We now have an average sentiment alongside the star ratings. If we’re right and sentiment analysis can predict a review’s opinion towards a restaurant, we should expect the sentiment score to correlate with the star rating.</p>
<p>Did it work?</p>
<figure>
<pre><code data-lang="r"><span>library</span><span>(</span><span>ggplot2</span><span>)</span><span>
</span><span>theme_set</span><span>(</span><span>theme_bw</span><span>())</span></code></pre>
</figure>
<figure>
<pre><code data-lang="r"><span>ggplot</span><span>(</span><span>reviews_sentiment</span><span>,</span><span> </span><span>aes</span><span>(</span><span>stars</span><span>,</span><span> </span><span>sentiment</span><span>,</span><span> </span><span>group</span><span> </span><span>=</span><span> </span><span>stars</span><span>))</span><span> </span><span>+</span><span>
</span><span>geom_boxplot</span><span>()</span><span> </span><span>+</span><span>
</span><span>ylab</span><span>(</span><span>&quot;Average sentiment score&quot;</span><span>)</span></code></pre>
</figure>
<p><img alt="center" src="http://i0.wp.com/varianceexplained.org/figs/2016-07-21-yelp-sentiment/unnamed-chunk-5-1.png?w=456" data-recalc-dims="1"></p>
<p>Well, it’s a very good start! Our sentiment scores are certainly correlated with positivity ratings. But we do see that there’s a large amount of prediction error- some 5-star reviews have a highly negative sentiment score, and vice versa.</p>
<h3>Which words are positive or negative?</h3>
<p>Our algorithm works at the word level, so if we want to improve our approach we should start there. Which words are suggestive of positive reviews, and which are negative?</p>
<p>To examine this, let’s create a per-word summary, and see which words tend to appear in positive or negative reviews. This takes more grouping and summarizing:</p>
<figure>
<pre><code data-lang="r"><span>review_words_counted</span><span> </span><span>&lt;-</span><span> </span><span>review_words</span><span> </span><span>%>%</span><span>
</span><span>count</span><span>(</span><span>review_id</span><span>,</span><span> </span><span>business_id</span><span>,</span><span> </span><span>stars</span><span>,</span><span> </span><span>word</span><span>)</span><span> </span><span>%>%</span><span>
</span><span>ungroup</span><span>()</span><span>
</span><span>review_words_counted</span></code></pre>
</figure>
<figure>
<pre><code data-lang="text">## # A tibble: 6,566,367 x 5
##                 review_id            business_id stars      word     n
##                     &lt;chr>                  &lt;chr> &lt;int>     &lt;chr> &lt;int>
## 1  ___XYEos-RIkPsQwplRYyw YxMnfznT3eYya0YV37tE8w     5    batter     1
## 2  ___XYEos-RIkPsQwplRYyw YxMnfznT3eYya0YV37tE8w     5     chips     3
## 3  ___XYEos-RIkPsQwplRYyw YxMnfznT3eYya0YV37tE8w     5  compares     1
## 4  ___XYEos-RIkPsQwplRYyw YxMnfznT3eYya0YV37tE8w     5 fashioned     1
## 5  ___XYEos-RIkPsQwplRYyw YxMnfznT3eYya0YV37tE8w     5  filleted     1
## 6  ___XYEos-RIkPsQwplRYyw YxMnfznT3eYya0YV37tE8w     5      fish     4
## 7  ___XYEos-RIkPsQwplRYyw YxMnfznT3eYya0YV37tE8w     5     fries     1
## 8  ___XYEos-RIkPsQwplRYyw YxMnfznT3eYya0YV37tE8w     5    frozen     1
## 9  ___XYEos-RIkPsQwplRYyw YxMnfznT3eYya0YV37tE8w     5 greenlake     1
## 10 ___XYEos-RIkPsQwplRYyw YxMnfznT3eYya0YV37tE8w     5      hand     1
## # ... with 6,566,357 more rows</code></pre>
</figure>
<figure>
<pre><code data-lang="r"><span>word_summaries</span><span> </span><span>&lt;-</span><span> </span><span>review_words_counted</span><span> </span><span>%>%</span><span>
</span><span>group_by</span><span>(</span><span>word</span><span>)</span><span> </span><span>%>%</span><span>
</span><span>summarize</span><span>(</span><span>businesses</span><span> </span><span>=</span><span> </span><span>n_distinct</span><span>(</span><span>business_id</span><span>),</span><span>
</span><span>reviews</span><span> </span><span>=</span><span> </span><span>n</span><span>(),</span><span>
</span><span>uses</span><span> </span><span>=</span><span> </span><span>sum</span><span>(</span><span>n</span><span>),</span><span>
</span><span>average_stars</span><span> </span><span>=</span><span> </span><span>mean</span><span>(</span><span>stars</span><span>))</span><span> </span><span>%>%</span><span>
</span><span>ungroup</span><span>()</span><span>
</span><span>word_summaries</span></code></pre>
</figure>
<figure>
<pre><code data-lang="text">## # A tibble: 100,177 x 5
##          word businesses reviews  uses average_stars
##         &lt;chr>      &lt;int>   &lt;int> &lt;int>         &lt;dbl>
## 1   a'boiling          1       1     1           4.0
## 2      a'fare          1       1     1           4.0
## 3      a'hole          1       1     1           5.0
## 4      a'ight          6       6     6           2.5
## 5        a'la          2       2     2           4.5
## 6        a'll          1       1     1           1.0
## 7      a'lyce          1       1     2           5.0
## 8      a'more          1       2     2           5.0
## 9    a'orange          1       1     1           5.0
## 10 a'prowling          1       1     1           3.0
## # ... with 100,167 more rows</code></pre>
</figure>
<p>We can start by looking only at words that appear in at least 200 (out of 200000) reviews. This makes sense both because rare words will have a noisier measurement (a few good or bad reviews could shift the balance), and because they’re less likely to be useful in classifying future reviews or text. I also filter for ones that appear in at least 10 businesses (others are likely to be specific to a particular restaurant).</p>
<figure>
<pre><code data-lang="r"><span>word_summaries_filtered</span><span> </span><span>&lt;-</span><span> </span><span>word_summaries</span><span> </span><span>%>%</span><span>
</span><span>filter</span><span>(</span><span>reviews</span><span> </span><span>>=</span><span> </span><span>200</span><span>,</span><span> </span><span>businesses</span><span> </span><span>>=</span><span> </span><span>10</span><span>)</span><span>
</span><span>word_summaries_filtered</span></code></pre>
</figure>
<figure>
<pre><code data-lang="text">## # A tibble: 4,328 x 5
##          word businesses reviews  uses average_stars
##         &lt;chr>      &lt;int>   &lt;int> &lt;int>         &lt;dbl>
## 1     ability        374     402   410      3.465174
## 2    absolute        808    1150  1183      3.710435
## 3  absolutely       2728    6158  6538      3.757389
## 4          ac        378     646   919      3.191950
## 5      accent        171     203   214      3.285714
## 6      accept        557     720   772      2.929167
## 7  acceptable        500     587   608      2.505963
## 8    accepted        293     321   332      2.968847
## 9      access        544     840   925      3.505952
## 10 accessible        220     272   282      3.816176
## # ... with 4,318 more rows</code></pre>
</figure>
<p>What were the most positive and negative words?</p>
<figure>
<pre><code data-lang="r"><span>word_summaries_filtered</span><span> </span><span>%>%</span><span>
</span><span>arrange</span><span>(</span><span>desc</span><span>(</span><span>average_stars</span><span>))</span></code></pre>
</figure>
<figure>
<pre><code data-lang="text">## # A tibble: 4,328 x 5
##             word businesses reviews  uses average_stars
##            &lt;chr>      &lt;int>   &lt;int> &lt;int>         &lt;dbl>
## 1  compassionate        193     298   312      4.677852
## 2        listens        177     215   218      4.632558
## 3       exceeded        286     320   321      4.596875
## 4       painless        224     290   294      4.568966
## 5   knowledgable        607     775   786      4.549677
## 6            gem        874    1703  1733      4.537874
## 7     impeccable        278     475   477      4.520000
## 8        happier        545     638   654      4.495298
## 9  knowledgeable       1550    2747  2807      4.493629
## 10   compliments        333     418   428      4.488038
## # ... with 4,318 more rows</code></pre>
</figure>
<p>Looks plausible to me! What about negative?</p>
<figure>
<pre><code data-lang="r"><span>word_summaries_filtered</span><span> </span><span>%>%</span><span>
</span><span>arrange</span><span>(</span><span>average_stars</span><span>)</span></code></pre>
</figure>
<figure>
<pre><code data-lang="text">## # A tibble: 4,328 x 5
##              word businesses reviews  uses average_stars
##             &lt;chr>      &lt;int>   &lt;int> &lt;int>         &lt;dbl>
## 1            scam        211     263   297      1.368821
## 2     incompetent        275     317   337      1.378549
## 3  unprofessional        748     921   988      1.380022
## 4       disgusted        251     283   292      1.381625
## 5          rudely        349     391   418      1.493606
## 6            lied        281     332   372      1.496988
## 7          refund        717     930  1229      1.545161
## 8    unacceptable        387     441   449      1.569161
## 9           worst       2574    5107  5597      1.569219
## 10        refused        803     983  1096      1.579858
## # ... with 4,318 more rows</code></pre>
</figure>
<p>Also makes a lot of sense. We can also plot positivity by frequency:</p>
<figure>
<pre><code data-lang="r"><span>ggplot</span><span>(</span><span>word_summaries_filtered</span><span>,</span><span> </span><span>aes</span><span>(</span><span>reviews</span><span>,</span><span> </span><span>average_stars</span><span>))</span><span> </span><span>+</span><span>
</span><span>geom_point</span><span>()</span><span> </span><span>+</span><span>
</span><span>geom_text</span><span>(</span><span>aes</span><span>(</span><span>label</span><span> </span><span>=</span><span> </span><span>word</span><span>),</span><span> </span><span>check_overlap</span><span> </span><span>=</span><span> </span><span>TRUE</span><span>,</span><span> </span><span>vjust</span><span> </span><span>=</span><span> </span><span>1</span><span>,</span><span> </span><span>hjust</span><span> </span><span>=</span><span> </span><span>1</span><span>)</span><span> </span><span>+</span><span>
</span><span>scale_x_log10</span><span>()</span><span> </span><span>+</span><span>
</span><span>geom_hline</span><span>(</span><span>yintercept</span><span> </span><span>=</span><span> </span><span>mean</span><span>(</span><span>reviews</span><span>$</span><span>stars</span><span>),</span><span> </span><span>color</span><span> </span><span>=</span><span> </span><span>&quot;red&quot;</span><span>,</span><span> </span><span>lty</span><span> </span><span>=</span><span> </span><span>2</span><span>)</span><span> </span><span>+</span><span>
</span><span>xlab</span><span>(</span><span>&quot;# of reviews&quot;</span><span>)</span><span> </span><span>+</span><span>
</span><span>ylab</span><span>(</span><span>&quot;Average Stars&quot;</span><span>)</span></code></pre>
</figure>
<p><img alt="center" src="http://i2.wp.com/varianceexplained.org/figs/2016-07-21-yelp-sentiment/word_summaries_filtered_plot-1.png?w=456" data-recalc-dims="1"></p>
<p>Note that some of the most common words (e.g. “food”) are pretty neutral. There are some common words that are pretty positive (e.g. “amazing”, “awesome”) and others that are pretty negative (“bad”, “told”).</p>
<h3>Comparing to sentiment analysis</h3>
<p>When we perform sentiment analysis, we’re typically comparing to a pre-existing lexicon, one that may have been developed for a particular purpose. That means that on our new dataset (Yelp reviews), some words may have different implications.</p>
<p>We can combine and compare the two datasets with <code>inner_join</code>.</p>
<figure>
<pre><code data-lang="r"><span>words_afinn</span><span> </span><span>&lt;-</span><span> </span><span>word_summaries_filtered</span><span> </span><span>%>%</span><span>
</span><span>inner_join</span><span>(</span><span>AFINN</span><span>)</span><span>
</span><span>words_afinn</span></code></pre>
</figure>
<figure>
<pre><code data-lang="text">## # A tibble: 505 x 6
##            word businesses reviews  uses average_stars afinn_score
##           &lt;chr>      &lt;int>   &lt;int> &lt;int>         &lt;dbl>       &lt;int>
## 1       ability        374     402   410      3.465174           2
## 2        accept        557     720   772      2.929167           1
## 3      accepted        293     321   332      2.968847           1
## 4      accident        369     447   501      3.536913          -2
## 5  accidentally        279     305   307      3.252459          -2
## 6        active        177     215   238      3.744186           1
## 7      adequate        420     502   527      3.203187           1
## 8         admit        942    1316  1348      3.620821          -1
## 9      admitted        196     248   271      2.157258          -1
## 10     adorable        305     416   431      4.281250           3
## # ... with 495 more rows</code></pre>
</figure>
<figure>
<pre><code data-lang="r"><span>ggplot</span><span>(</span><span>words_afinn</span><span>,</span><span> </span><span>aes</span><span>(</span><span>afinn_score</span><span>,</span><span> </span><span>average_stars</span><span>,</span><span> </span><span>group</span><span> </span><span>=</span><span> </span><span>afinn_score</span><span>))</span><span> </span><span>+</span><span>
</span><span>geom_boxplot</span><span>()</span><span> </span><span>+</span><span>
</span><span>xlab</span><span>(</span><span>&quot;AFINN score of word&quot;</span><span>)</span><span> </span><span>+</span><span>
</span><span>ylab</span><span>(</span><span>&quot;Average stars of reviews with this word&quot;</span><span>)</span></code></pre>
</figure>
<p><img alt="center" src="http://i2.wp.com/varianceexplained.org/figs/2016-07-21-yelp-sentiment/words_afinn-1.png?w=456" data-recalc-dims="1"></p>
<p>Just like in our per-review predictions, there’s a very clear trend. AFINN sentiment analysis works, at least a little bit!</p>
<p>But we may want to see some of those details. Which positive/negative words were most successful in predicting a positive/negative review, and which broke the trend?</p>
<p><img alt="center" src="http://i2.wp.com/varianceexplained.org/figs/2016-07-21-yelp-sentiment/words_afinn_plot-1.png?w=456" data-recalc-dims="1"></p>
<p>For example, we can see that most profanity has an AFINN score of -4, and that while some words, like “wtf”, successfully predict a negative review, others, like “damn”, are often positive (e.g. “the roast beef was <strong>damn</strong> good!”). Some of the words that AFINN most underestimated included “die” (“the pork chops are to <strong>die</strong> for!”), and one of the words it most overestimated was “joke” (“the service is a complete <strong>joke</strong>!”).</p>
<p>One other way we could look at misclassifications is to add AFINN sentiments to our frequency vs average stars plot:</p>
<p><img alt="center" src="http://i1.wp.com/varianceexplained.org/figs/2016-07-21-yelp-sentiment/word_summaries_filtered_plot_AFINN-1.png?w=456" data-recalc-dims="1"></p>
<p>One thing I like about the tidy text mining framework is that it lets us explore the successes and failures of our model at this granular level, using tools (ggplot2, dplyr) that we’re already familiar with.</p>
<h3>Next time: Machine learning</h3>
<p>In this post I’ve focused on basic exploration of the Yelp review dataset, and an evaluation of one sentiment analysis method for predicting review positivity. (Our conclusion: it’s good, but far from perfect!) But what if we want to create our own prediction method based on these reviews?</p>
<p>In my next post on this topic, I’ll show how to train LASSO regression (with the <a target="_blank" href="https://cran.r-project.org/web/packages/glmnet/index.html">glmnet</a> package) on this dataset to create a predictive model. This will serve as an introduction to machine learning methods in text classification. It will also let us create our own new “lexicon” of positive and negative words, one that may be more appropriate to our context of restaurant reviews.</p>
<div>
<ol>
<li>
<p>I encourage you to download this dataset and follow along- but note that if you do, you are bound by their <a target="_blank" href="https://www.yelp.com/html/pdf/Dataset_Challenge_Academic_Dataset_Agreement.pdf">Terms of Use</a>. <a target="_blank" href="http://varianceexplained.org/r/yelp-sentiment/#fnref:termsofuse">↩</a></p>
</li>
</ol>
</div>
<p><div><div><div><div data-width="55" data-show-faces="false" data-send="true" data-layout="box_count" data-height="62" data-href="http://www.r-bloggers.com/does-sentiment-analysis-work-a-tidy-analysis-of-yelp-reviews/"></div></div><div><a data-url="http://www.r-bloggers.com/does-sentiment-analysis-work-a-tidy-analysis-of-yelp-reviews/" data-counturl="http://www.r-bloggers.com/does-sentiment-analysis-work-a-tidy-analysis-of-yelp-reviews/" data-count="vertical" data-text="Does sentiment analysis work? A tidy analysis of Yelp reviews" class="twitter-share-button" data-via="rbloggers" href="https://twitter.com/share"></a></div><div></div></div><div></div></div>
<div>
<div>To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="http://varianceexplained.org/r/yelp-sentiment/"> Variance Explained</a></strong>.</div>
<hr>
<a rel="nofollow" href="http://www.r-bloggers.com/">R-bloggers.com</a> offers <strong><a rel="nofollow" href="http://feedburner.google.com/fb/a/mailverify?uri=RBloggers">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" rel="nofollow" href="http://www.r-project.org/">R</a> news and <a title="R tutorials" rel="nofollow" href="http://www.r-bloggers.com/?s=tutorial">tutorials</a> on topics such as: <a title="Data science" rel="nofollow" href="http://www.r-bloggers.com/?s=data%20science">Data science</a>, <a title="Big Data" rel="nofollow" href="http://www.r-bloggers.com/?s=Big%20Data">Big Data, <a title="R jobs" rel="nofollow" href="http://www.r-users.com/">R jobs</a>, visualization (<a title="ggplot and ggplot2 tutorials" rel="nofollow" href="http://www.r-bloggers.com/?s=ggplot2">ggplot2</a>, <a title="Boxplots using lattice and ggplot2 tutorials" rel="nofollow" href="http://www.r-bloggers.com/?s=boxplot">Boxplots</a>, <a title="Maps and gis" rel="nofollow" href="http://www.r-bloggers.com/?s=map">maps</a>, <a title="Animation in R" rel="nofollow" href="http://www.r-bloggers.com/?s=animation">animation</a>), programming (<a title="RStudio IDE for R" rel="nofollow" href="http://www.r-bloggers.com/?s=RStudio">RStudio</a>, <a title="Sweave and literate programming" rel="nofollow" href="http://www.r-bloggers.com/?s=sweave">Sweave</a>, <a title="LaTeX in R" rel="nofollow" href="http://www.r-bloggers.com/?s=LaTeX">LaTeX</a>, <a title="SQL and databases" rel="nofollow" href="http://www.r-bloggers.com/?s=SQL">SQL</a>, <a title="Eclipse IDE for R" rel="nofollow" href="http://www.r-bloggers.com/?s=eclipse">Eclipse</a>, <a title="git and github, Version Control System" rel="nofollow" href="http://www.r-bloggers.com/?s=git">git</a>, <a title="Large data in R using Hadoop" rel="nofollow" href="http://www.r-bloggers.com/?s=hadoop">hadoop</a>, <a title="Web Scraping of google, facebook, yahoo, twitter and more using R" rel="nofollow" href="http://www.r-bloggers.com/?s=Web+Scraping">Web Scraping</a>) statistics (<a title="Regressions and ANOVA analysis tutorials" rel="nofollow" href="http://www.r-bloggers.com/?s=regression">regression</a>, <a title="principal component analysis tutorial" rel="nofollow" href="http://www.r-bloggers.com/?s=PCA">PCA</a>, <a title="Time series" rel="nofollow" href="http://www.r-bloggers.com/?s=time+series">time series</a>, <a title="finance trading" rel="nofollow" href="http://www.r-bloggers.com/?s=trading">trading</a>) and more...
<img height="1" alt="" width="1" src="http://feeds.feedburner.com/~r/RBloggers/~4/TsbVTJ0QuK4"></a></div>