<p><a href="https://twitter.com/drsimonj">@drsimonj</a> here to share my method for ordering categories within facets to create plots that look like this…</p>
<p><a href="https://svbtleusercontent.com/bjcpgitl0gfhg.png"></a></p>
<p>instead of like this…</p>
<p><a href="https://svbtleusercontent.com/ikolwwigvjg3xg.png"></a></p>
<h2><a href="https://drsimonj.svbtle.com/#motivation-a-hrefhttptidytextminingcomtidy-te_2"> </a>Motivation: <a href="http://tidytextmining.com/">Tidy Text Mining in R</a></h2>
<p>The motivation for this post comes from <a href="http://tidytextmining.com/">Tidy Text Mining in R</a> by <a href="https://twitter.com/juliasilge">Julia Silge</a> and <a href="https://twitter.com/drob">David Robinson</a>. It is a <strong>must read</strong> if text mining is something that interests you.</p>
<p>I noticed that Julia and David had left themselves a “TODO” in <a href="http://tidytextmining.com/ngrams.html#tokenizing-by-n-gram">Chapter 5</a> that was “not easy to fix.” Not easy to fix? Could Julia Silge and David Robinson face challenges as the rest of us do?!</p>
<p><a href="https://svbtleusercontent.com/ha8tnnjrwwlurg.jpg"></a></p>
<p>Shocking, I know.</p>
<p>Well, it was probably just a matter of time until they fixed it. Still, I thought it was an interesting challenge; gave it some thought, and wanted to share my solution.</p>
<h2><a href="https://drsimonj.svbtle.com/#the-problem_2"> </a>The problem</h2>
<p>They were using <a href="https://cran.r-project.org/web/packages/ggplot2/index.html">ggplot2</a> to create a bar plot with the following features:</p>
<ul><li> Facetted into separate panels</li>
<li> One bar for each category (words in their case).</li>
<li> Each bar category (words) can appear in one or more facet panels with different bar heights.</li>
</ul><p>The problem, as you might recognize, is that varying bar heights for categories that appear in multiple facet panels makes it difficult to order the bars. Some within-facet ordering must take place, but how to do this?</p>
<p>Here, we’re going to roughly replicate the plot that presents this problem and then consider a potential solution.</p>
<h2><a href="https://drsimonj.svbtle.com/#the-problem-plot_2"> </a>The problem plot</h2>
<p>Using code from the book, the following roughly reproduces the problematic plot presented in <a href="http://tidytextmining.com/ngrams.html#tokenizing-by-n-gram">Chapter 5</a> of <a href="http://tidytextmining.com/">Tidy Text Mining in R</a>:</p>
<pre><code># Required libraries
library(dplyr)
library(tidyr)
library(ggplot2)
library(tidytext)
library(janeaustenr)

# From section 5.1: Tokenizing by n-gram
austen_bigrams &lt;- austen_books() %>%
  unnest_tokens(bigram, text, token = &quot;ngrams&quot;, n = 2)

# From section 5.1.1: Counting and filtering n-grams
bigrams_separated &lt;- austen_bigrams %>%
  separate(bigram, c(&quot;word1&quot;, &quot;word2&quot;), sep = &quot; &quot;)


# From section 5.1.3: Using bigrams to provide context in sentiment analysis
AFINN &lt;- get_sentiments(&quot;afinn&quot;)
negation_words &lt;- c(&quot;not&quot;, &quot;no&quot;, &quot;never&quot;, &quot;without&quot;)

negated_words &lt;- bigrams_separated %>%
  filter(word1 %in% negation_words) %>%
  inner_join(AFINN, by = c(word2 = &quot;word&quot;)) %>%
  count(word1, word2, score, sort = TRUE) %>%
  ungroup()

# Create plot
negated_words %>%
  mutate(contribution = n * score) %>%
  mutate(word2 = reorder(word2, contribution)) %>%
  group_by(word1) %>%
  top_n(10, abs(contribution)) %>%
  ggplot(aes(word2, contribution, fill = n * score > 0)) +
    geom_bar(stat = &quot;identity&quot;, show.legend = FALSE) +
    facet_wrap(~ word1, scales = &quot;free&quot;) +
    xlab(&quot;Words preceded by negation&quot;) +
    ylab(&quot;Sentiment score * # of occurrences&quot;) +
    theme_bw() +
    coord_flip()
</code></pre>
<p><a href="https://svbtleusercontent.com/xf6l0zgmyrxtfw.png"></a></p>
<p>This plot shows the words that are influencing the sentiment analysis of Jane Austen’s books but frequently preceded with the negation words “never,” “no,” “not,” or “without.” For example, Julia and David make the following conclusion based on the “not” facet:</p>
<blockquote><p>The bigrams “not like” and “not help” were overwhelmingly the largest causes of misidentification, making the text seem much more positive than it is.</p>
</blockquote>
<p>While the content is fascinating, our challenge is more general: to fix the ordering of the bars within each facet.</p>
<h2><a href="https://drsimonj.svbtle.com/#how-to-order-within-facets_2"> </a>How to order within facets</h2>
<p>When the data is prepared for plotting (code repeated below), the use of <code>reorder</code> is doing some ordering for us, but doesn’t produce a perfect result because of the facetting problem.</p>
<pre><code>negated_words %>%
  mutate(contribution = n * score) %>%
  # ----- >
  # This line does some reordering
  # but not perfect
  mutate(word2 = reorder(word2, contribution)) %>%
  # &lt; -----
  group_by(word1) %>%
  top_n(10, abs(contribution))
</code></pre>
<p>To overcome this, my first temptation was to try arranging the data within facets as a grouped data frame. However, this didn’t get me very far.</p>
<p>Instead, we can order the entire data set as follows:</p>
<ol><li> <code>ungroup</code> the data.</li>
<li><p><code>arrange</code> the data by:</p>
<ol><li> The variable for facet groups (<code>word1</code>)</li>
<li> The variable for bar heights (<code>contribution</code>)</li>
</ol></li>
<li><p>Keep track of this new arrangement with an <code>order</code> column of the <code>row_number</code>.</p></li>
</ol><p>Let’s do this and save the results:</p>
<pre><code># Plot Data Frame
pd &lt;- negated_words %>%
  mutate(contribution = n * score) %>%
  group_by(word1) %>%
  top_n(10, abs(contribution)) %>% 
  # 1. Remove grouping
  ungroup() %>%
  # 2. Arrange by
  #   i.  facet group
  #   ii. bar height
  arrange(word1, contribution) %>%
  # 3. Add order column of row numbers
  mutate(order = row_number())

pd
#> # A tibble: 50 × 6
#>    word1    word2 score     n contribution order
#>    &lt;chr>    &lt;chr> &lt;int> &lt;int>        &lt;int> &lt;int>
#> 1  never   failed    -2     8          -16     1
#> 2  never   forget    -1    12          -12     2
#> 3  never  failing    -2     4           -8     3
#> 4  never disgrace    -2     2           -4     4
#> 5  never    tired    -2     2           -4     5
#> 6  never    agree     1     4            4     6
#> 7  never  consent     2     2            4     7
#> 8  never     hope     2     2            4     8
#> 9  never     like     2     2            4     9
#> 10 never    worth     2     2            4    10
#> # ... with 40 more rows
</code></pre>
<p>This orders the entire data frame, but also orders the categories (words) <strong>within</strong> each facet group!</p>
<p>To demonstrate, let’s plot the results with <code>order</code> on the x-axis and without freeing the facet scales:</p>
<pre><code>ggplot(pd, aes(order, contribution, fill = n * score > 0)) +
  geom_bar(stat = &quot;identity&quot;, show.legend = FALSE) +
  facet_wrap(~ word1) +
  xlab(&quot;Words preceded by negation&quot;) +
  ylab(&quot;Sentiment score * # of occurrences&quot;) +
  theme_bw() +
  coord_flip()
</code></pre>
<p><a href="https://svbtleusercontent.com/r1jqg5cv428rmq.png"></a></p>
<p>See how we’ve accurately ordered the bars within each facet. The only thing is that the “starting point” is not 1 (except for the first facet). By freeing the scales again, we can properly focus each facet.</p>
<pre><code>ggplot(pd, aes(order, contribution, fill = n * score > 0)) +
  geom_bar(stat = &quot;identity&quot;, show.legend = FALSE) +
  # Free the scales here
  facet_wrap(~ word1, scales = &quot;free&quot;) +
  xlab(&quot;Words preceded by negation&quot;) +
  ylab(&quot;Sentiment score * # of occurrences&quot;) +
  theme_bw() +
  coord_flip()
</code></pre>
<p><a href="https://svbtleusercontent.com/udsesdkqpuv1fq.png"></a></p>
<h2><a href="https://drsimonj.svbtle.com/#adding-categories-to-the-axis_2"> </a>Adding categories to the axis</h2>
<p>The last piece of the puzzle is to replace the numeric values on each x-axis with the appropriate word. We can do this using <code>scale_x_continuous</code> to replace the <code>order</code> number with the corresponding category (<code>word2</code>) in the row. Aside, we’re controlling the x-axis, but the words appear on the y-axis because we’re using <code>coord_flip</code>.</p>
<pre><code>ggplot(pd, aes(order, contribution, fill = n * score > 0)) +
  geom_bar(stat = &quot;identity&quot;, show.legend = FALSE) +
  facet_wrap(~ word1, scales = &quot;free&quot;) +
  xlab(&quot;Words preceded by negation&quot;) +
  ylab(&quot;Sentiment score * # of occurrences&quot;) +
  theme_bw() +
  # Add categories to axis
  scale_x_continuous(
    breaks = pd$order,
    labels = pd$word2,
    expand = c(0,0)
  ) +
  coord_flip()
</code></pre>
<p><a href="https://svbtleusercontent.com/dmstymim0k0u9g.png"></a></p>
<p>Et voilà!</p>
<h2><a href="https://drsimonj.svbtle.com/#todo_2"> </a>TODO</h2>
<p>Before publishing this post, I sent a draft to Julia and David for comments. My sincerest thanks to them for taking the time to read it!</p>
<p>David also made the great suggestion to encapsulate this method into a function or add a new “stat” option to <code>geom_bar</code> to handle this automatically. I’ll be opening an issue about this on the <a href="https://github.com/tidyverse/ggplot2/issues">ggplot2 GitHub page</a> and playing around to see what works best.</p>
<p>How good is open source :)</p>
<h2><a href="https://drsimonj.svbtle.com/#sign-off_2"> </a>Sign off</h2>
<p>Thanks for reading and I hope this was useful for you.</p>
<p>For updates of recent blog posts, follow <a href="https://twitter.com/drsimonj">@drsimonj</a> on Twitter, or email me at <a href="mailto:drsimonjackson@gmail.com">drsimonjackson@gmail.com</a> to get in touch.</p>
<p>If you’d like the code that produced this blog, check out the <a href="https://github.com/drsimonj/blogR">blogR GitHub repository</a>.</p>