<p><div><div><div><div data-show-faces="false" data-height="21" data-width="100" data-layout="button_count" data-send="true" data-href="https://www.r-bloggers.com/the-social-weather-of-ropensci-onboarding-system/"></div></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="https://ropensci.org/blog/2018/05/10/onboarding-social-weather/"> rOpenSci - open tools for open science</a></strong>, and kindly contributed to <a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers)</a> </div>
<p>Our <a rel="nofollow" href="https://github.com/ropensci/onboarding/" target="_blank">onboarding process</a><br>
ensures that packages contributed by the community undergo a<br>
transparent, constructive, non adversarial and open review process.<br>
Before even submitting my first R package to rOpenSci onboarding system<br>
in December 2015, I spent a fair amount of time reading through previous<br>
issue threads in order to assess whether onboarding was a friendly place<br>
for me: a newbie, very motivated to learn more but a newbie nonetheless.<br>
I soon got the feeling that yes, onboarding would help me make my<br>
package better without ever making me feel inadequate.</p>
<p>More recently, I read <a rel="nofollow" href="https://github.com/Open-Advice/Open-Advice/blob/c3e03b669f7ea7b20ef98378210cdbc8a7ea7edc/documentationsupport/AnneGentle.tex" target="_blank">Anne Gentle’s<br>
essay</a><br>
in Open Advice where she mentions the concept of the <em>social weather</em> of<br>
an online community. By listening before I even posted, I was simply<br>
trying to get a good idea of the social weather of onboarding – as a<br>
side-note, how wonderful is it that onboarding’s being open makes it<br>
possible to listen?!</p>
<p>In the meantime, I’ve now only submitted and reviewed a few packages but<br>
also become an <a rel="nofollow" href="https://github.com/ropensci/onboarding/#associate-editors" target="_blank">associate<br>
editor</a>. In<br>
general, when one of us editors talks about onboarding, we like to use<br>
examples illustrating the system in a good light: often excerpts from<br>
review processes, or quotes of tweets by authors or reviewers. Would<br>
there be a more quantitative way for us to support our promotion<br>
efforts? In this blog post, I shall explore how a<br>
<a rel="nofollow" href="https://www.tidytextmining.com/" target="_blank">tidytext</a> analysis of review processes<br>
(via their GitHub threads) might help us characterize the social weather<br>
of onboarding.</p>
<h3>Preparing the data</h3>
<h4>A note about text mining</h4>
<p>In this blog post, I’ll leverage the <code>tidytext</code> package, with the help<br>
of its accompanying book <a rel="nofollow" href="https://www.tidytextmining.com/" target="_blank">“Tidy text<br>
mining”</a>. The authors, Julia Silge and<br>
David Robinson, actually met and started working on this project, at<br>
<a rel="nofollow" href="http://unconf16.ropensci.org/" target="_blank">rOpenSci unconf in 2016</a>!</p>
<h4>The “tidy text” of the analysis</h4>
<p>I’ve described <a rel="nofollow" href="https://ropensci.org/blog/2018/04/26/rectangling-onboarding/" target="_blank">in the first post of this<br>
series</a><br>
how I got all onboarding issue threads. Now, I’ll explain how I cleaned<br>
their text. Why does it need to be cleaned? Well, this text contains<br>
elements that I wished to remove: code chunks, as well as parts from our<br>
submission and <a rel="nofollow" href="https://github.com/ropensci/onboarding/blob/master/reviewer_template.md" target="_blank">review<br>
templates</a>.</p>
<p>My biggest worry was the removal of templates from issues. I was already<br>
picturing my spending hours writing regular expressions to remove these<br>
lines… and then I realized that the word “lines” was the key! I could go<br>
split all issue comments into <em>lines</em>, which is called tokenization in<br>
proper text mining vocabulary, and then remove duplicates! This way, I<br>
didn’t even have to worry about the templates having changed a bit over<br>
time, since each version was used at least twice. A tricky part that<br>
remained was the removal of code chunks since I only wanted to keep<br>
human conversation. In theory, it was easy: code chunks are lines<br>
located between two lines containing ““`”… I’m still not sure I<br>
solved this in the easiest possible way.</p>
<pre><code>library(&quot;magrittr&quot;)
threads &lt;- readr::read_csv(&quot;data/clean_data.csv&quot;)
</code></pre>
<pre><code># to remove code lines between ```
range &lt;- function(x1, x2){
  x1:x2
}

# I need the indices of lines between ```
split_in_indices &lt;- function(x){
  lengthx &lt;- length(x)
  if(length(x) == 0){
    return(0)
  }else{
    if(lengthx > 2){
      
      limits1 &lt;- x[seq(from = 1, to = (lengthx - 1), by = 2)]
      limits2 &lt;- x[seq(from = 2, to = lengthx, by = 2)]
      purrr::map2(limits1, limits2, range) %>%
        unlist() %>%
        c()
    }else{
      x[1]:x[2]
    }
  }
}

# tokenize by line
threads &lt;- tidytext::unnest_tokens(threads, line, body, token = &quot;lines&quot;)

# remove white space
threads &lt;- dplyr::mutate(threads, line = trimws(line))

# remove citations lines
threads &lt;- dplyr::filter(threads, !stringr::str_detect(line, &quot;^\\>&quot;))

# remove the line from the template that has ``` that used to bother me
threads &lt;- dplyr::filter(threads, !stringr::str_detect(line, &quot;bounded by ```&quot;))

# correct one line
threads &lt;- dplyr::mutate(threads, line = stringr::str_replace_all(line, &quot;`` `&quot;, &quot;```&quot;))

# group by comment 
threads &lt;- dplyr::group_by(threads, title, created_at, user, issue)

# get indices
threads &lt;- dplyr::mutate(threads, index = 1:n())

# get lines limiting chunks
threads &lt;- dplyr::mutate(threads, chunk_limit = stringr::str_detect(line, &quot;```&quot;)&amp;stringr::str_count(line, &quot;`&quot;) %in% c(3, 4))

# special treatment
threads &lt;- dplyr::mutate(threads,
                     chunk_limit = ifelse(user == &quot;MarkEdmondson1234&quot; &amp; issue == 127 &amp; index == 33,
                                          FALSE, chunk_limit))
threads &lt;- dplyr::mutate(threads, which_limit = list(split_in_indices(which(chunk_limit))))

# weird code probably to get indices of code lines
threads &lt;- dplyr::mutate(threads, code = index %in% which_limit[[1]])
threads &lt;- dplyr::ungroup(threads)
</code></pre>
<p>Let’s look at what this does in practice, with comments from<br>
<a rel="nofollow" href="https://github.com/ropensci/onboarding/issues/41" target="_blank"><code>gutenbergr</code><br>
submission</a> as<br>
example. I chose this submission because the package author, David<br>
Robinson, is one of the two <code>tidytext</code> creators, and because I was the<br>
reviewer, so it’s all very meta, isn’t it?</p>
<p>In the excerpt below, we see the most important variable, the binary<br>
<code>code</code> indicating whether the line is a code line. This excerpt also<br>
shows variables created to help compute <code>code</code>: <code>index</code> is the index of<br>
the line withing a comment, <code>chunk_limit</code> indicates whether the line is<br>
a chunk limit, <code>which_limit</code> indicates which indices in the comment<br>
indicate lines of code.</p>
<pre><code>dplyr::filter(threads, package == &quot;gutenbergr&quot;, 
              user == &quot;sckott&quot;, 
              !stringr::str_detect(line, &quot;ropensci..footer&quot;)) %>%
  dplyr::mutate(created_at = as.character(created_at)) %>%
  dplyr::select(created_at, line, code, index, chunk_limit, which_limit) %>%
  knitr::kable()
</code></pre>
<table>
<thead>
<tr>
<th align="left">created_at</th>
<th align="left">line</th>
<th align="left">code</th>
<th align="right">index</th>
<th align="left">chunk_limit</th>
<th align="left">which_limit</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">2016-05-02 17:04:56</td>
<td align="left">thanks for your submission @dgrtwo – seeking reviewers now</td>
<td align="left">FALSE</td>
<td align="right">1</td>
<td align="left">FALSE</td>
<td align="left">0</td>
</tr>
<tr>
<td align="left">2016-05-04 06:09:19</td>
<td align="left">reviewers: @masalmon</td>
<td align="left">FALSE</td>
<td align="right">1</td>
<td align="left">FALSE</td>
<td align="left">0</td>
</tr>
<tr>
<td align="left">2016-05-04 06:09:19</td>
<td align="left">due date: 2016-05-24</td>
<td align="left">FALSE</td>
<td align="right">2</td>
<td align="left">FALSE</td>
<td align="left">0</td>
</tr>
<tr>
<td align="left">2016-05-12 16:16:38</td>
<td align="left">having a quick look over this now…</td>
<td align="left">FALSE</td>
<td align="right">1</td>
<td align="left">FALSE</td>
<td align="left">0</td>
</tr>
<tr>
<td align="left">2016-05-12 16:45:59</td>
<td align="left">@dgrtwo looks great. just a minor thing:</td>
<td align="left">FALSE</td>
<td align="right">1</td>
<td align="left">FALSE</td>
<td align="left">3:7</td>
</tr>
<tr>
<td align="left">2016-05-12 16:45:59</td>
<td align="left"><code>gutenberg_get_mirror()</code> throws a warning due to <code>xml2</code>, at this line <a rel="nofollow" href="https://github.com/dgrtwo/gutenbergr/blob/master/r/gutenberg_download.r#l213" target="_blank">https://github.com/dgrtwo/gutenbergr/blob/master/r/gutenberg_download.r#l213</a></td>
<td align="left">FALSE</td>
<td align="right">2</td>
<td align="left">FALSE</td>
<td align="left">3:7</td>
</tr>
<tr>
<td align="left">2016-05-12 16:45:59</td>
<td align="left">“` r</td>
<td align="left">TRUE</td>
<td align="right">3</td>
<td align="left">TRUE</td>
<td align="left">3:7</td>
</tr>
<tr>
<td align="left">2016-05-12 16:45:59</td>
<td align="left">warning message:</td>
<td align="left">TRUE</td>
<td align="right">4</td>
<td align="left">FALSE</td>
<td align="left">3:7</td>
</tr>
<tr>
<td align="left">2016-05-12 16:45:59</td>
<td align="left">in node_find_one(x*n<strong>o</strong>d*<em>e</em>, *x*doc, xpath = xpath, nsmap = ns) :</td>
<td align="left">TRUE</td>
<td align="right">5</td>
<td align="left">FALSE</td>
<td align="left">3:7</td>
</tr>
<tr>
<td align="left">2016-05-12 16:45:59</td>
<td align="left">101 matches for .//a: using first</td>
<td align="left">TRUE</td>
<td align="right">6</td>
<td align="left">FALSE</td>
<td align="left">3:7</td>
</tr>
<tr>
<td align="left">2016-05-12 16:45:59</td>
<td align="left">“`</td>
<td align="left">TRUE</td>
<td align="right">7</td>
<td align="left">TRUE</td>
<td align="left">3:7</td>
</tr>
<tr>
<td align="left">2016-05-12 16:45:59</td>
<td align="left">wonder if it’s worth a <code>suppresswarnings()</code> there?</td>
<td align="left">FALSE</td>
<td align="right">8</td>
<td align="left">FALSE</td>
<td align="left">3:7</td>
</tr>
<tr>
<td align="left">2016-05-12 20:42:53</td>
<td align="left">great!</td>
<td align="left">FALSE</td>
<td align="right">1</td>
<td align="left">FALSE</td>
<td align="left">3:5</td>
</tr>
<tr>
<td align="left">2016-05-12 20:42:53</td>
<td align="left">– add the footer to your readme:</td>
<td align="left">FALSE</td>
<td align="right">2</td>
<td align="left">FALSE</td>
<td align="left">3:5</td>
</tr>
<tr>
<td align="left">2016-05-12 20:42:53</td>
<td align="left">“`</td>
<td align="left">TRUE</td>
<td align="right">3</td>
<td align="left">TRUE</td>
<td align="left">3:5</td>
</tr>
<tr>
<td align="left">2016-05-12 20:42:53</td>
<td align="left">“`</td>
<td align="left">TRUE</td>
<td align="right">5</td>
<td align="left">TRUE</td>
<td align="left">3:5</td>
</tr>
<tr>
<td align="left">2016-05-12 20:42:53</td>
<td align="left">– could you add <code>url</code> and <code>bugreports</code> entries to <code>description</code>, so people know where to get sources and report bugs/issues</td>
<td align="left">FALSE</td>
<td align="right">6</td>
<td align="left">FALSE</td>
<td align="left">3:5</td>
</tr>
<tr>
<td align="left">2016-05-12 20:42:53</td>
<td align="left">– update installation of dev versions to <code>ropenscilabs/gutenbergr</code> and any urls for the github repo to <code>ropenscilabs</code> instead of <code>dgrtwo</code></td>
<td align="left">FALSE</td>
<td align="right">7</td>
<td align="left">FALSE</td>
<td align="left">3:5</td>
</tr>
<tr>
<td align="left">2016-05-12 20:42:53</td>
<td align="left">– go to the repo settings –> transfer ownership and transfer to <code>ropenscilabs</code> – note that all our newer pkgs go to <code>ropenscilabs</code> first, then when more mature we’ll move to <code>ropensci</code></td>
<td align="left">FALSE</td>
<td align="right">8</td>
<td align="left">FALSE</td>
<td align="left">3:5</td>
</tr>
<tr>
<td align="left">2016-05-13 01:22:22</td>
<td align="left">nice, builds on at travis <a rel="nofollow" href="https://travis-ci.org/ropenscilabs/gutenbergr/" target="_blank">https://travis-ci.org/ropenscilabs/gutenbergr/</a> – you can keep appveyor builds under your acct, or i can start on mine, let me know</td>
<td align="left">FALSE</td>
<td align="right">1</td>
<td align="left">FALSE</td>
<td align="left">0</td>
</tr>
<tr>
<td align="left">2016-05-13 16:06:31</td>
<td align="left">updated badge link, started an appveyor account with <code>ropenscilabs</code> as account name – sent pr – though the build is failing, something about getting the current gutenberg url <a rel="nofollow" href="https://ci.appveyor.com/project/sckott/gutenbergr/build/1.0.1#l650" target="_blank">https://ci.appveyor.com/project/sckott/gutenbergr/build/1.0.1#l650</a></td>
<td align="left">FALSE</td>
<td align="right">1</td>
<td align="left">FALSE</td>
<td align="left">0</td>
</tr>
<tr>
<td align="left">
</td>
<td align="left"></td>
<td align="left"></td>
<td align="right"></td>
<td align="left"></td>
<td align="left"></td>
</tr>
</tbody>
</table>
<p>So as you see now getting rid of chunks is straightforward: the lines<br>
with <code>code == TRUE</code> have to be deleted.</p>
<pre><code># remove them and get rid of now useless columns
threads &lt;- dplyr::filter(threads, !code)
threads &lt;- dplyr::select(threads, - code, - which_limit, - index, - chunk_limit)
</code></pre>
<p>Now on to removing template parts… I noticed that removing duplicates<br>
was a bit too drastic because sometimes duplicates were poorly formatted<br>
citations, e.g. an author answering a reviewer’s question by<br>
copy-pasting it without <a rel="nofollow" href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#blockquotes" target="_blank">Markdown<br>
blockquotes</a>,<br>
in which case we definitely want to keep the first occurrence. Besides,<br>
duplicates were sometimes very short sentences such as “great!” that are<br>
not templates, that we therefore should keep. Therefore, for each line,<br>
I counted how many times it occurred overall (<code>no_total_occ</code>), and in<br>
how many issues it occurred (<code>no_issues</code>).</p>
<p>Let’s look at <a rel="nofollow" href="https://github.com/ropensci/onboarding/issues/118#issuecomment-310503322" target="_blank">Joseph Stachelek’s review of<br>
<code>rrricanes</code></a><br>
for instance.</p>
<pre><code>dplyr::filter(threads, user == &quot;jsta&quot;, is_review) %>%
  dplyr::select(line) %>%
  head() %>%
  knitr::kable()
</code></pre>
<table>
<colgroup>
<col>
</colgroup>
<thead>
<tr>
<th>line</th>
</tr>
</thead>
<tbody>
<tr>
<td>## package review</td>
</tr>
<tr>
<td>– [x] as the reviewer i confirm that there are no conflicts of interest for me to review this work (if you are unsure whether you are in conflict, please speak to your editor <em>before</em> starting your review).</td>
</tr>
<tr>
<td>#### documentation</td>
</tr>
<tr>
<td>the package includes all the following forms of documentation:</td>
</tr>
<tr>
<td>– [x] <strong>a statement of need</strong> clearly stating problems the software is designed to solve and its target audience in readme</td>
</tr>
<tr>
<td>– [x] <strong>installation instructions:</strong> for the development version of package and any non-standard dependencies in readme</td>
</tr>
</tbody>
</table>
<p>Now if we clean up a bit…</p>
<pre><code>threads &lt;- dplyr::group_by(threads, line)
threads &lt;- dplyr::mutate(threads, no_total_occ = n(),
                     no_issues = length(unique(issue)), 
                     size = stringr::str_length(line))
threads &lt;- dplyr::ungroup(threads)
threads &lt;- dplyr::group_by(threads, issue, line)
threads &lt;- dplyr::arrange(threads, created_at)
threads &lt;- dplyr::filter(threads, no_total_occ &lt;= 2,
                     # for repetitions in total keep the short ones 
                     # bc they are stuff like &quot;thanks&quot; so not template
                     # yes 10 is arbitrary
                     no_issues &lt;= 1 | size &lt; 10)
# when there's a duplicate in one issue
# it's probably citation
# so keep the first occurrence
get_first &lt;- function(x){
  x[1]
}
threads &lt;- dplyr::group_by(threads, issue, line)
threads &lt;- dplyr::summarise_all(threads, get_first)
threads &lt;- dplyr::select(threads, - no_total_occ, - size, - no_issues)

threads &lt;- dplyr::mutate(threads, # let code words now be real words
                     line = stringr::str_replace_all(line, &quot;`&quot;, &quot;&quot;),
                     # only keep text from links, not the links themselves
                     line = stringr::str_replace_all(line, &quot;\\]\\(.*\\)&quot;, &quot;&quot;),
                     line = stringr::str_replace_all(line, &quot;\\[&quot;, &quot;&quot;),
                     line = stringr::str_replace_all(line, &quot;blob\\/master&quot;, &quot;&quot;), 
                     # ’
                     line = stringr::str_replace_all(line, &quot;’&quot;, &quot;&quot;), 
                     # remove some other links
                     line = stringr::str_replace_all(line, &quot;https\\:\\/\\/github\\.com\\/&quot;, &quot;&quot;))

threads &lt;- dplyr::filter(threads, !stringr::str_detect(line, &quot;estimated hours spent reviewing&quot;))
threads &lt;- dplyr::filter(threads, !stringr::str_detect(line, &quot;notifications@github\\.com&quot;))
threads &lt;- dplyr::filter(threads, !stringr::str_detect(line, &quot;reply to this email directly, view it on&quot;))
threads &lt;- dplyr::ungroup(threads)
</code></pre>
<p>Here is what we get from the same review.</p>
<pre><code>dplyr::filter(threads, user == &quot;jsta&quot;, is_review)  %>%
  dplyr::select(line) %>%
  head() %>%
  knitr::kable()
</code></pre>
<table>
<colgroup>
<col>
</colgroup>
<thead>
<tr>
<th>line</th>
</tr>
</thead>
<tbody>
<tr>
<td>* also, you might consider using the skip_on_cran function for lines that call an external download as recommended by the ropensci packaging guide.</td>
</tr>
<tr>
<td>* i am having timeout issues with building the getting_started vignette. i wonder if there is a particular year with very few hurricanes that would solve the timeout problem.</td>
</tr>
<tr>
<td>* i cannot build the data.world vignette. probably because i don’t have an api key set up. you may want to consider setting the code chunks to eval=false.</td>
</tr>
<tr>
<td>* i really like the tidy_ functions. i wonder if it would make it easier on the end-user to have the get_ functions return tidy results by default with an optional argument to return “messy” results.</td>
</tr>
<tr>
<td>* missing a maintainer field in the description</td>
</tr>
<tr>
<td>* there are no examples for knots_to_mph, mb_to_in, status_abbr_to_str, get_discus, get_fstadv, tidy_fstadv, tidy_wr, tidy_fcst. maybe some can be changed to non-exported?</td>
</tr>
</tbody>
</table>
<p>So now, we mostly got the interesting human and original language.</p>
<p>This got me “tidy enough” text. Let’s not mention this package author<br>
who found a way to poorly format <a rel="nofollow" href="https://github.com/ropensci/onboarding/issues/24" target="_blank">their<br>
submission</a> right<br>
under a guideline explaining how to copy the DESCRIPTION… Yep, that’s<br>
younger me. Oh well.</p>
<h4>Computing sentiment</h4>
<p>Another data preparation part was to compute the sentiment score of each<br>
line via the <a rel="nofollow" href="https://github.com/trinker/sentimentr" target="_blank"><code>sentimentr</code></a><br>
package by Tyler Rinker, which computes a score for sentences, not for<br>
single words.</p>
<pre><code>sentiment &lt;- all %>%
  dplyr::group_by(line, created_at, user, role, issue) %>%
  dplyr::mutate(sentiment = median(sentimentr::sentiment(line)$sentiment)) %>%
  dplyr::ungroup() %>%
  dplyr::select(line, created_at, user, role, issue, sentiment)
</code></pre>
<p>This dataset of sentiment will be used later in the post.</p>
<h3>Tidy text analysis of onboarding social weather</h3>
<h4>What do reviews talk about?</h4>
<p>To find out what reviews deal with as if I didn’t know about our<br>
guidelines, I’ll compute the frequency of words and bigrams, and the<br>
pairwise correlation of words within issue comments.</p>
<p>My using lollipops below was inspired by <a rel="nofollow" href="https://tonyelhabr.rbind.io/posts/tidy-text-analysis-google-search-history/" target="_blank">this fascinating blog post of<br>
Tony ElHabr’s about his Google search<br>
history</a>.</p>
<pre><code>library(&quot;ggplot2&quot;)
library(&quot;ggalt&quot;)
library(&quot;hrbrthemes&quot;)

stopwords &lt;- rcorpora::corpora(&quot;words/stopwords/en&quot;)$stopWords

word_counts &lt;- threads %>%
  tidytext::unnest_tokens(word, line) %>%
  dplyr::filter(!word %in% stopwords) %>%
  dplyr::count(word, sort = TRUE) %>%
  dplyr::mutate(word = reorder(word, n))   

ggplot(word_counts[1:15,]) +
  geom_lollipop(aes(word, n),
                size = 2, col = &quot;salmon&quot;) +
  hrbrthemes::theme_ipsum(base_size = 16,
                          axis_title_size = 16) +
  coord_flip()
</code></pre>
<p><img alt="Most common words in onboarding review
threads" src="https://i0.wp.com/ropensci.org/img/blog-images/2018-05-10-onboarding-social-weather/unnamed-chunk-9-1.png?w=456&ssl=1" data-recalc-dims="1"></p>
<pre><code>bigrams_counts &lt;- threads %>%
  tidytext::unnest_tokens(bigram, line, token = &quot;ngrams&quot;, n = 2) %>%
  tidyr::separate(bigram, c(&quot;word1&quot;, &quot;word2&quot;), sep = &quot; &quot;,
                  remove = FALSE) %>%
  dplyr::filter(!word1 %in% stopwords) %>%
  dplyr::filter(!word2 %in% stopwords) %>%
  dplyr::count(bigram, sort = TRUE) %>%
  dplyr::mutate(bigram = reorder(bigram, n))   

ggplot(bigrams_counts[2:15,]) +
  geom_lollipop(aes(bigram, n),
                size = 2, col = &quot;salmon&quot;) +
  hrbrthemes::theme_ipsum(base_size = 16,
                          axis_title_size = 16) +
  coord_flip()
</code></pre>
<p><img alt="Most common bigrams in onboarding review
threads" src="https://i0.wp.com/ropensci.org/img/blog-images/2018-05-10-onboarding-social-weather/unnamed-chunk-10-1.png?w=456&ssl=1" data-recalc-dims="1"></p>
<p>I’m not showing the first bigram that basically shows I’ve an encoding<br>
issue to solve with a variation of “´”. In any case, both figures show<br>
what we care about, like our guidelines that are mentioned often, and<br>
documentation. I think words absent from the figures such as performance<br>
or speed also highlight what we care less about, following <a rel="nofollow" href="https://github.com/jtleek/rpackages#documentation" target="_blank">Jeff Leek’s<br>
philosophy</a>.</p>
<p>Now, let’s move on to a bit more complex visualization of <a rel="nofollow" href="https://www.tidytextmining.com/ngrams.html#counting-and-correlating-pairs-of-words-with-the-widyr-package" target="_blank">pairwise<br>
correlations between<br>
words</a><br>
within lines. First, let’s prepare the table of words in lines. Compared<br>
with <a rel="nofollow" href="https://www.tidytextmining.com/ngrams.html#counting-and-correlating-pairs-of-words-with-the-widyr-package" target="_blank">the book<br>
tutorial</a>,<br>
we add a condition for eliminating words mentioned in only one<br>
submission, often function names.</p>
<pre><code>users &lt;- unique(threads$user)
onboarding_line_words &lt;- threads %>%
  dplyr::group_by(user, issue, created_at, package, line) %>%
  dplyr::mutate(line_id = paste(package, user, created_at, line)) %>%
  dplyr::ungroup() %>%
  tidytext::unnest_tokens(word, line) %>%
  dplyr::filter( word != package, !word %in% users,
                 is.na(as.numeric(word)),
                 word != &quot;ldecicco&quot;,
                 word != &quot;usgs&quot;) %>%
  dplyr::group_by(word) %>%
  dplyr::filter(length(unique(issue)) > 1) %>%
  dplyr::select(line_id, word)

onboarding_line_words %>%
  head() %>%
  knitr::kable()
</code></pre>
<table>
<thead>
<tr>
<th align="left">line_id</th>
<th align="left">word</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">rrlite karthik 2015-04-12 20:56:04 – ] add a ropensci footer.</td>
<td align="left">add</td>
</tr>
<tr>
<td align="left">rrlite karthik 2015-04-12 20:56:04 – ] add a ropensci footer.</td>
<td align="left">a</td>
</tr>
<tr>
<td align="left">rrlite karthik 2015-04-12 20:56:04 – ] add a ropensci footer.</td>
<td align="left">ropensci</td>
</tr>
<tr>
<td align="left">rrlite karthik 2015-04-12 20:56:04 – ] add a ropensci footer.</td>
<td align="left">footer</td>
</tr>
<tr>
<td align="left">rrlite karthik 2015-04-12 20:56:04 – ] add an appropriate entry into ropensci.org/packages/index.html</td>
<td align="left">add</td>
</tr>
<tr>
<td align="left">rrlite karthik 2015-04-12 20:56:04 – ] add an appropriate entry into ropensci.org/packages/index.html</td>
<td align="left">an</td>
</tr>
<tr>
<td align="left">
</td>
<td align="left"></td>
</tr>
</tbody>
</table>
<p>Then, we can compute the correlation.</p>
<pre><code>word_cors &lt;- onboarding_line_words %>%
  dplyr::group_by(word) %>%
  dplyr::filter(!word %in% stopwords) %>%
  dplyr::filter(n() >= 20) %>%
  widyr::pairwise_cor(word, line_id, sort = TRUE)
</code></pre>
<p>For instance, what often goes in the same line as vignette?</p>
<pre><code>dplyr::filter(word_cors, item1 == &quot;vignette&quot;)
</code></pre>
<pre><code>## # A tibble: 853 x 3
##    item1    item2     correlation
##    </code></pre>