<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/all-the-badges-one-can-earn-parsing-badges-of-cran-packages-readmes/"></div></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="https://ropensci.org/technotes/2018/09/10/github-badges/"> 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>A while ago we<br>
<a rel="nofollow" href="https://github.com/ropensci/onboarding/issues/130" target="_blank">onboarded</a> an<br>
exciting package, <a rel="nofollow" href="https://github.com/ropensci/codemetar/" target="_blank"><code>codemetar</code></a><br>
by <a rel="nofollow" href="https://ropensci.org/about/#team" target="_blank">Carl Boettiger</a>. <code>codemetar</code> is an R specific<br>
information collector and parser for the <a rel="nofollow" href="https://github.com/codemeta" target="_blank">CodeMeta<br>
project</a>. In particular, <code>codemetar</code> can<br>
digest metadata about an R package in order to fill <a rel="nofollow" href="https://codemeta.github.io/terms/" target="_blank">the terms<br>
recognized by CodeMeta</a>. This means<br>
extracting information from DESCRIPTION but also from e.g. continuous<br>
integration[1] badges in the README! In this note, we’ll take advantage<br>
of <code>codemetar::extract_badges</code> function to explore the diversity of<br>
badges worn by the READMEs of CRAN packages.</p>
<h2>Why <code>codemetar::extract_badges</code>, and how</h2>
<p>CodeMeta recognized terms include information we’ve been getting from<br>
badges:</p>
<ul>
<li>
<p>“contIntegration”, URLs to continuous integration services such as<br>
Travis, Appveyor, Codecov;</p>
</li>
<li>
<p>“review”, information about review of the software if there was one.<br>
<code>codemetar</code> recognizes information from the peer-review badge we add<br>
to the README of onboarded packages thanks to work by <a rel="nofollow" href="https://ropensci.org/about/#team" target="_blank">Karthik<br>
Ram</a>.</p>
</li>
</ul>
<p>The list might get longer, so instead of using regular expressions on<br>
the README text, we extract and memoize[2] all badges at once to a<br>
data.frame that we then query. The badges extraction is based on (in the<br>
dev branch of <code>codemetar</code>):</p>
<ul>
<li>
<p>conversion to XML using <code>commonmark</code> (see <a rel="nofollow" href="https://ropensci.org/technotes/2018/09/05/commonmark/" target="_blank">my recent tech<br>
note</a>) which works well for the<br>
Markdown badges e.g.<br>
<code>[![Travis-CI Build Status](https://travis-ci.org/ropensci/codemetar.svg?branch=master)](https://travis-ci.org/ropensci/codemetar)</code>.</p>
</li>
<li>
<p>extraction and parsing on the first HTML table there is if there’s<br>
any, which is necessary for <a rel="nofollow" href="https://github.com/ropensci/onboarding-meta/issues/56" target="_blank">badges in table, which we’ve been<br>
discussing for our<br>
guidelines</a>.</p>
</li>
</ul>
<p>Note that the CRAN version of <code>codemetar</code> already features<br>
<code>extract_badges</code>, but with a badges table creation based on regular<br>
expressions only. Here is <code>codemetar::extract_badges</code> in action <a rel="nofollow" href="https://github.com/ropensci/drake" target="_blank">on the<br>
README of the <code>drake</code> package</a>:</p>
<pre><code>library(&quot;magrittr&quot;)
codemetar::extract_badges(&quot;https://raw.githubusercontent.com/ropensci/drake/master/README.md&quot;) %>%
  knitr::kable()
</code></pre>
<table>
<thead>
<tr>
<th align="left">text</th>
<th align="left">link</th>
<th align="left">image_link</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">ropensci_footer</td>
<td align="left"><a rel="nofollow" href="https://ropensci.org/" target="_blank">https://ropensci.org</a></td>
<td align="left"><a rel="nofollow" href="http://ropensci.org/public_images/github_footer.png" target="_blank">http://ropensci.org/public_images/github_footer.png</a></td>
</tr>
<tr>
<td align="left">JOSS</td>
<td align="left"><a rel="nofollow" href="https://doi.org/10.21105/joss.00550" target="_blank">https://doi.org/10.21105/joss.00550</a></td>
<td align="left"><a rel="nofollow" href="http://joss.theoj.org/papers/10.21105/joss.00550/status.svg" target="_blank">http://joss.theoj.org/papers/10.21105/joss.00550/status.svg</a></td>
</tr>
<tr>
<td align="left">Licence</td>
<td align="left"><a rel="nofollow" href="https://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank">https://www.gnu.org/licenses/gpl-3.0.en.html</a></td>
<td align="left"><a rel="nofollow" href="https://img.shields.io/badge/licence-GPL--3-blue.svg" target="_blank">https://img.shields.io/badge/licence-GPL–3-blue.svg</a></td>
</tr>
<tr>
<td align="left">AppVeyor</td>
<td align="left"><a rel="nofollow" href="https://ci.appveyor.com/project/ropensci/drake" target="_blank">https://ci.appveyor.com/project/ropensci/drake</a></td>
<td align="left"><a rel="nofollow" href="https://ci.appveyor.com/api/projects/status/4ypc9xnmqt70j94e?svg=true&branch=master" target="_blank">https://ci.appveyor.com/api/projects/status/4ypc9xnmqt70j94e?svg=true&amp;branch=master</a></td>
</tr>
<tr>
<td align="left">rOpenSci</td>
<td align="left"><a rel="nofollow" href="https://github.com/ropensci/onboarding/issues/156" target="_blank">https://github.com/ropensci/onboarding/issues/156</a></td>
<td align="left"><a rel="nofollow" href="https://badges.ropensci.org/156_status.svg" target="_blank">https://badges.ropensci.org/156_status.svg</a></td>
</tr>
<tr>
<td align="left">minimal R version</td>
<td align="left"><a rel="nofollow" href="https://cran.r-project.org/" target="_blank">https://cran.r-project.org/</a></td>
<td align="left"><a rel="nofollow" href="https://img.shields.io/badge/R%3E%3D-3.3.0-blue.svg" target="_blank">https://img.shields.io/badge/R%3E%3D-3.3.0-blue.svg</a></td>
</tr>
<tr>
<td align="left">Travis</td>
<td align="left"><a rel="nofollow" href="https://travis-ci.org/ropensci/drake" target="_blank">https://travis-ci.org/ropensci/drake</a></td>
<td align="left"><a rel="nofollow" href="https://travis-ci.org/ropensci/drake.svg?branch=master" target="_blank">https://travis-ci.org/ropensci/drake.svg?branch=master</a></td>
</tr>
<tr>
<td align="left">CRAN</td>
<td align="left"><a rel="nofollow" href="http://cran.r-project.org/package=drake" target="_blank">http://cran.r-project.org/package=drake</a></td>
<td align="left"><a rel="nofollow" href="http://www.r-pkg.org/badges/version/drake" target="_blank">http://www.r-pkg.org/badges/version/drake</a></td>
</tr>
<tr>
<td align="left">downloads</td>
<td align="left"><a rel="nofollow" href="http://cran.rstudio.com/package=drake" target="_blank">http://cran.rstudio.com/package=drake</a></td>
<td align="left"><a rel="nofollow" href="http://cranlogs.r-pkg.org/badges/drake" target="_blank">http://cranlogs.r-pkg.org/badges/drake</a></td>
</tr>
<tr>
<td align="left">Codecov</td>
<td align="left"><a rel="nofollow" href="https://codecov.io/github/ropensci/drake?branch=master" target="_blank">https://codecov.io/github/ropensci/drake?branch=master</a></td>
<td align="left"><a rel="nofollow" href="https://codecov.io/github/ropensci/drake/coverage.svg?branch=master" target="_blank">https://codecov.io/github/ropensci/drake/coverage.svg?branch=master</a></td>
</tr>
<tr>
<td align="left">Zenodo</td>
<td align="left"><a rel="nofollow" href="https://zenodo.org/badge/latestdoi/82609103" target="_blank">https://zenodo.org/badge/latestdoi/82609103</a></td>
<td align="left"><a rel="nofollow" href="https://zenodo.org/badge/82609103.svg" target="_blank">https://zenodo.org/badge/82609103.svg</a></td>
</tr>
<tr>
<td align="left">Project Status: Active â€“ The project has reached a stable, usable state and is being actively developed.</td>
<td align="left"><a rel="nofollow" href="http://www.repostatus.org/#active" target="_blank">http://www.repostatus.org/#active</a></td>
<td align="left"><a rel="nofollow" href="http://www.repostatus.org/badges/latest/active.svg" target="_blank">http://www.repostatus.org/badges/latest/active.svg</a></td>
</tr>
</tbody>
</table>
<p>Quite handy for our metadata collection!</p>
<p>Read the source for <code>extract_badges</code><br>
<a rel="nofollow" href="https://github.com/ropensci/codemetar/blob/dev/R/extract_badges.R" target="_blank">here</a><br>
and see how it’s used in <a rel="nofollow" href="https://github.com/ropensci/codemetar/blob/dev/R/guess_metadata.R" target="_blank">this<br>
script</a>.<br>
You can also compare <a rel="nofollow" href="https://github.com/ropensci/codemetar/blob/master/README.md" target="_blank"><code>codemetar</code>’s<br>
README</a><br>
with its<br>
<a rel="nofollow" href="https://github.com/ropensci/codemetar/blob/master/codemeta.json" target="_blank">codemeta.json</a><br>
e.g. <a rel="nofollow" href="https://github.com/ropensci/codemetar/blob/master/codemeta.json#L445" target="_blank">the lines for<br>
“contIntegration”</a>.</p>
<p>Now since, <code>codemetar::extract_badges</code> exports a nice data.frame for any<br>
README with badges, and is exported, it’d be too bad not to use it to<br>
gain insights from many, many READMEs!</p>
<h2>Extract badges from CRAN packages</h2>
<p>In this exploration we shall concentrate on CRAN packages that indicate<br>
a GitHub repo link under the URL field of DESCRIPTION. By the way, if<br>
you don’t indicate such links in DESCRIPTION of your package yet, you<br>
can (and should) run<br>
<a rel="nofollow" href="http://usethis.r-lib.org/reference/use_github_links.html" target="_blank"><code>usethis::use_github_links</code></a>.</p>
<h3>Get links to GitHub repos from CRAN information</h3>
<p>I reckon that I could have also used BugReports like Steven M Mortimer<br>
did <a rel="nofollow" href="https://stevenmortimer.com/most-starred-r-packages-on-github/" target="_blank">in his great analysis of CRAN downloads and GitHub<br>
stars</a>. I<br>
am unsure of how one can get the link to and the content of the README<br>
of packages that don’t Rbuildignore their README, such as<br>
<a rel="nofollow" href="https://cran.r-project.org/web/packages/codemetar/index.html" target="_blank"><code>codemetar</code></a><br>
(see under Material). The imperfect sample I collected will do for this<br>
note.</p>
<p>Here’s how I got all the repo owners and names:</p>
<pre><code>cran_db &lt;- tools::CRAN_package_db()

# only packages that have a GitHub repo
github_cran &lt;- dplyr::filter(cran_db[, c(&quot;Package&quot;, &quot;URL&quot;)],
                             stringr::str_detect(URL, &quot;github\\.com&quot;))

# will need to keep only the URL to the repo
select_github_repo &lt;- function(URL){
  URLs &lt;- stringr::str_split(URL, pattern = &quot;,&quot;, simplify = TRUE)
  github_repo &lt;- URLs[stringr::str_detect(URLs, &quot;github\\.com&quot;)][1]
  github_repo &lt;- stringr::str_remove(github_repo, &quot;\\#.*$&quot;)
  github_repo &lt;- stringr::str_remove(github_repo, &quot;\\#.*[ \\(.*\\)]&quot;)
  github_repo &lt;- stringr::str_remove(github_repo, &quot;/$&quot;)
  stringr::str_replace(github_repo,&quot;.*\\.com\\/&quot;, &quot;&quot;)
}


github_cran &lt;- dplyr::group_by(github_cran, Package)
github_cran &lt;- dplyr::mutate(github_cran, github = select_github_repo(URL))
github_cran &lt;- tidyr::separate(github_cran, github, &quot;\\/&quot;, 
                               into = c(&quot;owner&quot;, &quot;repo&quot;))
github_cran &lt;- dplyr::ungroup(github_cran)

# Not very general
github_cran$repo[which(github_cran$Package == &quot;webp&quot;)] &lt;- &quot;webp&quot;
</code></pre>
<p>I needed a bit of string cleaning mostly to deal with the URLs of<br>
<a rel="nofollow" href="https://ropensci.org/about/#team" target="_blank">Jeroen Ooms</a>’ packages, see e.g. <a rel="nofollow" href="https://cran.r-project.org/web/packages/antiword/index.html" target="_blank">this<br>
one</a>. I<br>
guess I could have cleaned even more, but it was good enough for this<br>
exploration.</p>
<h3>Get all badges</h3>
<p>Then for each repo I queried the download URL of the <a rel="nofollow" href="https://developer.github.com/v3/repos/contents/#get-the-readme" target="_blank">preferred<br>
README</a><br>
via GitHub’s V3 API, using <a rel="nofollow" href="https://github.com/r-lib/gh" target="_blank">the <code>gh</code><br>
package</a>. The preferred README is the one<br>
GitHub displays on the repo landing page. I used<br>
<code>codemetar::extract_badges</code>, of course. I rate-limited the basic<br>
function using <a rel="nofollow" href="https://github.com/tarakc02/ratelimitr" target="_blank"><code>ratelimitr</code></a>.</p>
<pre><code>library(&quot;magrittr&quot;)

github_cran &lt;- readr::read_csv(&quot;data/github_cran_links.csv&quot;)

.get_badges &lt;- function(owner, repo){
  message(paste(owner, repo, sep = &quot;/&quot;))
  readme &lt;- try(gh::gh(&quot;GET /repos/:owner/:repo/readme&quot;,
                       owner = owner, repo = repo),
                silent = TRUE)
  if(inherits(readme, &quot;try-error&quot;)){
    return(NULL)
  }else{
    
    badges &lt;- codemetar::extract_badges(readme$download_url)
    
    if(nrow(badges)>0){
      badges$owner &lt;- owner
      badges$repo &lt;- repo
    }
    
    return(badges)
  }
  
  
}

.get_badges %>%
  ratelimitr::limit_rate(ratelimitr::rate(1, 1)) -> get_badges

purrr::map2_df(github_cran$owner, 
               github_cran$repo,
              get_badges) -> badges
</code></pre>
<h3>Remove non badges from the sample</h3>
<p>The way badges are recognized by <code>codemetar::extract_badges</code> is not<br>
specific enough, it can include images formatted like badges that aren’t<br>
badges but instead either local images or images whose credit is shown<br>
as URL. To remove them from the sample, I used a strategy in two steps:</p>
<ul>
<li>
<p>I first had a look at the most common domains. For the 17 most<br>
common of them, I accepted the images except for one, ropensci.org,<br>
included because the <em>footer</em> our packages get is formatted as a<br>
Markdown badge.</p>
</li>
<li>
<p>For the remaining images, a bit more than 200, I used <code>magick</code> to<br>
obtain their width and height, and filtered actual badges based on<br>
their width/height ratio. Sometimes the link to the image wasn’t<br>
even valid, which was also a reason for exclusion, since it revealed<br>
the image was a local one.</p>
</li>
</ul>
<pre><code># extract and parse URLs
badges %>%
  dplyr::pull(image_link) %>%
  purrr::map_df(urltools::url_parse) -> parsed_image_links

# count hits by domain

parsed_image_links %>%
  dplyr::count(domain, sort = TRUE) -> domain_count

# these were manually inspected
# as legit badge providers
ok_domain &lt;- domain_count$domain[1:17]

# keep the badges needing a check
tbd &lt;- dplyr::filter(parsed_image_links,
                     ! domain %in% ok_domain)
                     
# get their size ratio
get_size &lt;- function(url){
  img &lt;- try(magick::image_read(url),
             silent = TRUE)
  
  if(inherits(img, &quot;try-error&quot;)){
    tibble::tibble(error = TRUE,
                   image_link = url)
  }else{
    info &lt;- magick::image_info(img)
    info$error &lt;- FALSE
    info$image_link &lt;- url
    info
  }
  
  
}

img_info &lt;- purrr::map_df(urltools::url_compose(tbd),
                          get_size)

img_info &lt;- dplyr::mutate(img_info, ratio = width/height)

# filter badges from images
img_info &lt;- dplyr::filter(img_info, 
                          ratio &lt; 3|error)

# it'd have been wiser to use a row-wise workflow!
badges &lt;- dplyr::filter(badges,
                        !image_link %in% img_info$image_link,
                        !image_link %in% stringr::str_remove(img_info$image_link,
                                                                      &quot;/$&quot;),
                        !tolower(image_link) %in% img_info$image_link,
                        !tolower(image_link) %in% stringr::str_remove(img_info$image_link,
                                                                      &quot;/$&quot;),
                        !stringr::str_detect(image_link,
                                             &quot;ropensci\\.org\\/public\\_images\\/&quot;))
readr::write_csv(badges, &quot;data/aaall_badges.csv&quot;)
</code></pre>
<p>Don’t judge me by my filenaming skills. I was <em>maybe</em> a bit too<br>
enthusiastic!</p>
<h2>Analyze badges from CRAN packages</h2>
<p>I wanted to answer several questions about the badges of CRAN packages,<br>
beyond being just happy to have been able to collect so many of them.</p>
<h3>How many repos have at least one badge?</h3>
<pre><code>github_cran &lt;- readr::read_csv(&quot;data/github_cran_links.csv&quot;)
# the same repo can have been used by several packages!
badges &lt;- readr::read_csv(&quot;data/aaall_badges.csv&quot;)
badges &lt;- dplyr::distinct(badges)

nobadges &lt;- dplyr::anti_join(github_cran, badges,
                             by = c(&quot;owner&quot;, &quot;repo&quot;))
</code></pre>
<p>There are 1277 packages without any badge (or rather said, without any<br>
badge that we identified) out of a sample of 3541 packages. That means<br>
64% have at least one badge. As a reminder, there are more than 13,000<br>
packages on CRAN so we’re only looking at a subset.</p>
<h3>Among the repos with badges, how many badges?</h3>
<pre><code>library(&quot;magrittr&quot;)
badges %>%
  dplyr::count(repo, owner,
               sort = TRUE) -> badges_count

badges_count %>%
  dplyr::summarise(median = median(n))
</code></pre>
<pre><code>## # A tibble: 1 x 1
##   median
##    </code></pre>