purrr
library(tidyverse)
library(jsonlite)
library(curl)
library(repurrrsive)
library(listviewer)
Not all lists are easily coerced into data frames by simply calling fromJSON() %>% as_tibble()
. Unless your list is perfectly structured, this will not work. Recall the OMDB example:
# omdb API function
omdb <- function(Key, Title, Year, Plot, Format){
baseurl <- "http://www.omdbapi.com/?"
params <- c("apikey=", "t=", "y=", "plot=", "r=")
values <- c(Key, Title, Year, Plot, Format)
param_values <- map2_chr(params, values, str_c)
args <- str_c(param_values, collapse = "&")
str_c(baseurl, args)
}
# use curl to execute the query
request_sharknado <- omdb(getOption("omdb_key"), "Sharknado", "2013", "short", "json")
con <- curl(request_sharknado)
answer_json <- readLines(con)
close(con)
# convert to data frame
answer_json %>%
fromJSON() %>%
as_tibble()
## Error: Column `Ratings` must be a 1d atomic vector or a list
Wait a minute, what happened? Look at the structure of answer_json %>% fromJSON()
:
sharknado <- answer_json %>%
fromJSON()
str(sharknado)
## List of 25
## $ Title : chr "Sharknado"
## $ Year : chr "2013"
## $ Rated : chr "TV-14"
## $ Released : chr "11 Jul 2013"
## $ Runtime : chr "86 min"
## $ Genre : chr "Comedy, Horror, Sci-Fi"
## $ Director : chr "Anthony C. Ferrante"
## $ Writer : chr "Thunder Levin"
## $ Actors : chr "Ian Ziering, Tara Reid, John Heard, Cassandra Scerbo"
## $ Plot : chr "When a freak hurricane swamps Los Angeles, nature's deadliest killer rules sea, land, and air as thousands of s"| __truncated__
## $ Language : chr "English"
## $ Country : chr "USA"
## $ Awards : chr "1 win & 2 nominations."
## $ Poster : chr "https://images-na.ssl-images-amazon.com/images/M/MV5BOTE2OTk4MTQzNV5BMl5BanBnXkFtZTcwODUxOTM3OQ@@._V1_SX300.jpg"
## $ Ratings :'data.frame': 2 obs. of 2 variables:
## ..$ Source: chr [1:2] "Internet Movie Database" "Rotten Tomatoes"
## ..$ Value : chr [1:2] "3.3/10" "82%"
## $ Metascore : chr "N/A"
## $ imdbRating: chr "3.3"
## $ imdbVotes : chr "38,247"
## $ imdbID : chr "tt2724064"
## $ Type : chr "movie"
## $ DVD : chr "03 Sep 2013"
## $ BoxOffice : chr "N/A"
## $ Production: chr "NCM Fathom"
## $ Website : chr "http://www.mtivideo.com/TitleView.aspx?TITLE_ID=728"
## $ Response : chr "True"
jsonedit(sharknado, mode = "view", elementId = "sharknado")
Look at the ratings
element: it is a data frame. Remember that data frames are just a special type of list, so what we have here is a list inside of a list (aka a recursive list). We cannot easily flatten this into a data frame, because the ratings
element is not an atomic vector of length 1 like all the other elements in sharknado
. Instead, we have to think of another way to convert it to a data frame.
We need to load two packages now: repurrrsive
contains examples of recursive lists, and listviewer
which provides an interactive method for viewing the structure of a list.
devtools::install_github("jennybc/repurrrsive")
install.packages("listviewer")
library(purrr)
library(repurrrsive)
Before you can apply functions to a list, you should understand it. Especially when dealing with poorly documented APIs, you may not know in advance the structure of your list, or it may not be the same as the documentation. str()
is the base R method for inspecting a list by printing the structure of the list to the console. If you have a large list, this will be a lot of output. max.levels
and list.len
can be used to print only a partial structure for this list. Alternatively, you can use jsonedit()
to interactively view the list within RStudio.
Let’s look at got_chars
, which is a list of information on the 29 point-of-view characters from the first five books in A Song of Ice and Fire by George R.R. Martin.
Spoiler alert - if you haven’t read the series, you may not want to read too much into each list element. That said, the book series is over 20 years old now and the show Game of Thrones is incredibly popular, so you’ve had plenty of opportunity to learn this information by now.
Each element corresponds to one character and contains 18 sub-elements which are named atomic vectors of various lengths and types.
str(got_chars, list.len = 3)
## List of 29
## $ :List of 18
## ..$ url : chr "http://www.anapioficeandfire.com/api/characters/1022"
## ..$ id : int 1022
## ..$ name : chr "Theon Greyjoy"
## .. [list output truncated]
## $ :List of 18
## ..$ url : chr "http://www.anapioficeandfire.com/api/characters/1052"
## ..$ id : int 1052
## ..$ name : chr "Tyrion Lannister"
## .. [list output truncated]
## $ :List of 18
## ..$ url : chr "http://www.anapioficeandfire.com/api/characters/1074"
## ..$ id : int 1074
## ..$ name : chr "Victarion Greyjoy"
## .. [list output truncated]
## [list output truncated]
jsonedit(got_chars, mode = "view", elementId = "got_chars")
purrr::map()
We can use purrr::map()
to extract elements from lists.
Let’s extract the name
element for each Game of Thrones character. To do this, we can use map()
and extract list elements based on their name:
map(got_chars[1:4], "name")
## [[1]]
## [1] "Theon Greyjoy"
##
## [[2]]
## [1] "Tyrion Lannister"
##
## [[3]]
## [1] "Victarion Greyjoy"
##
## [[4]]
## [1] "Will"
A companion shortcut is to extract elements by their integer position in the list. For example, extract the 3rd element of each character’s list like so:
map(got_chars[5:8], 3)
## [[1]]
## [1] "Areo Hotah"
##
## [[2]]
## [1] "Chett"
##
## [[3]]
## [1] "Cressen"
##
## [[4]]
## [1] "Arianne Martell"
To recap, here are two shortcuts for making the .f
function that map()
will apply:
function(x) x[["TEXT"]]
i
to extract the i
-th element
function(x) x[[i]]
And as always, we can use map()
with the pipe %>%
:
got_chars %>%
map("name")
got_chars %>%
map(3)
map()
always returns a list, but if you know that the elements are all the same type (e.g. numeric, character, boolean) and are each of length one, you can use the map_()
function appropriate for that type of vector.
map_chr(got_chars[9:12], "name")
## [1] "Daenerys Targaryen" "Davos Seaworth" "Arya Stark"
## [4] "Arys Oakheart"
map_chr(got_chars[13:16], 3)
## [1] "Asha Greyjoy" "Barristan Selmy" "Varamyr" "Brandon Stark"
What if you want to retrieve elements? What if you want to know the character’s name and gender? For a single user, we can use traditional subsetting:
# Victarion element
got_chars[[3]]
## $url
## [1] "http://www.anapioficeandfire.com/api/characters/1074"
##
## $id
## [1] 1074
##
## $name
## [1] "Victarion Greyjoy"
##
## $gender
## [1] "Male"
##
## $culture
## [1] "Ironborn"
##
## $born
## [1] "In 268 AC or before, at Pyke"
##
## $died
## [1] ""
##
## $alive
## [1] TRUE
##
## $titles
## [1] "Lord Captain of the Iron Fleet" "Master of the Iron Victory"
##
## $aliases
## [1] "The Iron Captain"
##
## $father
## [1] ""
##
## $mother
## [1] ""
##
## $spouse
## [1] ""
##
## $allegiances
## [1] "House Greyjoy of Pyke"
##
## $books
## [1] "A Game of Thrones" "A Clash of Kings" "A Storm of Swords"
##
## $povBooks
## [1] "A Feast for Crows" "A Dance with Dragons"
##
## $tvSeries
## list()
##
## $playedBy
## list()
# specific elements for Victarion
got_chars[[3]][c("name", "culture", "gender", "born")]
## $name
## [1] "Victarion Greyjoy"
##
## $culture
## [1] "Ironborn"
##
## $gender
## [1] "Male"
##
## $born
## [1] "In 268 AC or before, at Pyke"
We use a single square bracket indexing and a character vector to index by name. To adapt this to the map()
framework, recall:
map(.x, .f, ...)
The function .f
will be [
and ...
will be the character vector identifying the names of the elements to extract.
x <- map(got_chars, `[`, c("name", "culture", "gender", "born"))
str(x[16:17])
## List of 2
## $ :List of 4
## ..$ name : chr "Brandon Stark"
## ..$ culture: chr "Northmen"
## ..$ gender : chr "Male"
## ..$ born : chr "In 290 AC, at Winterfell"
## $ :List of 4
## ..$ name : chr "Brienne of Tarth"
## ..$ culture: chr ""
## ..$ gender : chr "Female"
## ..$ born : chr "In 280 AC"
Alternatively, we can use magrittr::extract()
to do the same thing. It looks a bit more clean:
library(magrittr)
##
## Attaching package: 'magrittr'
## The following object is masked from 'package:purrr':
##
## set_names
## The following object is masked from 'package:tidyr':
##
## extract
x <- map(got_chars, extract, c("name", "culture", "gender", "born"))
str(x[18:19])
## List of 2
## $ :List of 4
## ..$ name : chr "Catelyn Stark"
## ..$ culture: chr "Rivermen"
## ..$ gender : chr "Female"
## ..$ born : chr "In 264 AC, at Riverrun"
## $ :List of 4
## ..$ name : chr "Cersei Lannister"
## ..$ culture: chr "Westerman"
## ..$ gender : chr "Female"
## ..$ born : chr "In 266 AC, at Casterly Rock"
Notice that even by extracting multiple elements at once, we are still left with a list. But we want a simplified data frame! Remember that the output of map()
is always a list. To force the output to be a data frame, use map_df()
:
map_df(got_chars, extract, c("name", "culture", "gender", "id", "born", "alive"))
## Warning in flatten_bindable(dots_values(...)): '.Random.seed' is not an
## integer vector but of type 'NULL', so ignored
## # A tibble: 29 x 6
## name culture gender id
## <chr> <chr> <chr> <int>
## 1 Theon Greyjoy Ironborn Male 1022
## 2 Tyrion Lannister Male 1052
## 3 Victarion Greyjoy Ironborn Male 1074
## 4 Will Male 1109
## 5 Areo Hotah Norvoshi Male 1166
## 6 Chett Male 1267
## 7 Cressen Male 1295
## 8 Arianne Martell Dornish Female 130
## 9 Daenerys Targaryen Valyrian Female 1303
## 10 Davos Seaworth Westeros Male 1319
## # ... with 19 more rows, and 2 more variables: born <chr>, alive <lgl>
Now we have an automatically type converted data frame. It was quite simple to perform, however it is not very robust. It takes more code, but it is generally better to explicitly specify the type of each column to ensure the output is as you would expect:
got_chars %>% {
tibble(
name = map_chr(., "name"),
culture = map_chr(., "culture"),
gender = map_chr(., "gender"),
id = map_int(., "id"),
born = map_chr(., "born"),
alive = map_lgl(., "alive")
)
}
## # A tibble: 29 x 6
## name culture gender id
## <chr> <chr> <chr> <int>
## 1 Theon Greyjoy Ironborn Male 1022
## 2 Tyrion Lannister Male 1052
## 3 Victarion Greyjoy Ironborn Male 1074
## 4 Will Male 1109
## 5 Areo Hotah Norvoshi Male 1166
## 6 Chett Male 1267
## 7 Cressen Male 1295
## 8 Arianne Martell Dornish Female 130
## 9 Daenerys Targaryen Valyrian Female 1303
## 10 Davos Seaworth Westeros Male 1319
## # ... with 19 more rows, and 2 more variables: born <chr>, alive <lgl>
The dot
.
above is the placeholder for the primary input:got_chars
in this case. The curly braces{}
surrounding thetibble()
call preventgot_chars
from being passed in as the first argument oftibble()
.
gh_users
repurrsive
provides information on 6 GitHub users in a list named gh_users
. It is a recursive list:
What is in the list? Let’s take a look:
str(gh_users, list.len = 3)
## List of 6
## $ :List of 30
## ..$ login : chr "gaborcsardi"
## ..$ id : int 660288
## ..$ avatar_url : chr "https://avatars.githubusercontent.com/u/660288?v=3"
## .. [list output truncated]
## $ :List of 30
## ..$ login : chr "jennybc"
## ..$ id : int 599454
## ..$ avatar_url : chr "https://avatars.githubusercontent.com/u/599454?v=3"
## .. [list output truncated]
## $ :List of 30
## ..$ login : chr "jtleek"
## ..$ id : int 1571674
## ..$ avatar_url : chr "https://avatars.githubusercontent.com/u/1571674?v=3"
## .. [list output truncated]
## [list output truncated]
jsonedit(gh_users, mode = "view", elementId = "gh_users")
Extract each user’s real name, username, GitHub ID, location, date of account creation, and number of public repositories. Store this information in a tidy data frame.
Using the easy (non-robust method):
map_df(gh_users, `[`, c("login", "name", "id", "location", "created_at", "public_repos"))
## # A tibble: 6 x 6
## login name id location
## <chr> <chr> <int> <chr>
## 1 gaborcsardi Gábor Csárdi 660288 Chippenham, UK
## 2 jennybc Jennifer (Jenny) Bryan 599454 Vancouver, BC, Canada
## 3 jtleek Jeff L. 1571674 Baltimore,MD
## 4 juliasilge Julia Silge 12505835 Salt Lake City, UT
## 5 leeper Thomas J. Leeper 3505428 London, United Kingdom
## 6 masalmon Maëlle Salmon 8360597 Barcelona, Spain
## # ... with 2 more variables: created_at <chr>, public_repos <int>
Using the longer (but robust) way:
gh_users %>% {
tibble(
login = map_chr(., "login"),
name = map_chr(., "name"),
id = map_int(., "id"),
location = map_chr(., "location"),
created_at = map_chr(., "created_at") %>%
lubridate::ymd_hms(),
public_repos = map_int(., "public_repos")
)
}
## # A tibble: 6 x 6
## login name id location
## <chr> <chr> <int> <chr>
## 1 gaborcsardi Gábor Csárdi 660288 Chippenham, UK
## 2 jennybc Jennifer (Jenny) Bryan 599454 Vancouver, BC, Canada
## 3 jtleek Jeff L. 1571674 Baltimore,MD
## 4 juliasilge Julia Silge 12505835 Salt Lake City, UT
## 5 leeper Thomas J. Leeper 3505428 London, United Kingdom
## 6 masalmon Maëlle Salmon 8360597 Barcelona, Spain
## # ... with 2 more variables: created_at <dttm>, public_repos <int>
Also notice that because I extracted each element manually, I could easily convert created_at
to a datetime column.
gh_users
has a single primary level of nesting, but you regularly will encounter even more levels. gh_repos
is a list with:
str(gh_repos, list.len = 2)
## List of 6
## $ :List of 30
## ..$ :List of 68
## .. ..$ id : int 61160198
## .. ..$ name : chr "after"
## .. .. [list output truncated]
## ..$ :List of 68
## .. ..$ id : int 40500181
## .. ..$ name : chr "argufy"
## .. .. [list output truncated]
## .. [list output truncated]
## $ :List of 30
## ..$ :List of 68
## .. ..$ id : int 14756210
## .. ..$ name : chr "2013-11_sfu"
## .. .. [list output truncated]
## ..$ :List of 68
## .. ..$ id : int 14152301
## .. ..$ name : chr "2014-01-27-miami"
## .. .. [list output truncated]
## .. [list output truncated]
## [list output truncated]
jsonedit(gh_repos, mode = "view", elementId = "gh_repos")
Now we use the indexing shortcuts in a more complicated setting. Instead of providing a single name or position, we use a vector:
j
-th element addresses the j
-th level of the hierarchyHere we get the full name (element 3) of the first repository listed for each user.
gh_repos %>%
map_chr(c(1, 3))
## [1] "gaborcsardi/after" "jennybc/2013-11_sfu" "jtleek/advdatasci"
## [4] "juliasilge/2016-14" "leeper/ampolcourse" "masalmon/aqi_pdf"
Note that this does NOT give elements 1 and 3 of gh_repos
. It extracts the first repo for each user and, within that, the 3rd piece of information for the repo.
Our objective is to get a data frame with one row per repository, with variables identifying which GitHub user owns it, the repository name, etc.
gh_repos
First let’s create a data frame with gh_repos
as a list-column along with identifying GitHub usernames. To do this, we extract the user names using the approach outlined above, set them as the names on gh_repos
, then convert the named list into a tibble:
(unames <- map_chr(gh_repos, c(1, 4, 1)))
## [1] "gaborcsardi" "jennybc" "jtleek" "juliasilge" "leeper"
## [6] "masalmon"
(udf <- gh_repos %>%
set_names(unames) %>%
enframe("username", "gh_repos"))
## # A tibble: 6 x 2
## username gh_repos
## <chr> <list>
## 1 gaborcsardi <list [30]>
## 2 jennybc <list [30]>
## 3 jtleek <list [30]>
## 4 juliasilge <list [26]>
## 5 leeper <list [30]>
## 6 masalmon <list [30]>
Next let’s extract some basic piece of information from gh_repos
. For instance, how many repos are associated with each user?
udf %>%
mutate(n_repos = map_int(gh_repos, length))
## # A tibble: 6 x 3
## username gh_repos n_repos
## <chr> <list> <int>
## 1 gaborcsardi <list [30]> 30
## 2 jennybc <list [30]> 30
## 3 jtleek <list [30]> 30
## 4 juliasilge <list [26]> 26
## 5 leeper <list [30]> 30
## 6 masalmon <list [30]> 30
Before attempting to map()
functions to the entire data frame, let’s first practice on a single user.
# one_user is a list of repos for one user
one_user <- udf$gh_repos[[1]]
# one_user[[1]] is a list of info for one repo
one_repo <- one_user[[1]]
str(one_repo, max.level = 1, list.len = 5)
## List of 68
## $ id : int 61160198
## $ name : chr "after"
## $ full_name : chr "gaborcsardi/after"
## $ owner :List of 17
## $ private : logi FALSE
## [list output truncated]
# a highly selective list of tibble-worthy info for one repo
one_repo[c("name", "fork", "open_issues")]
## $name
## [1] "after"
##
## $fork
## [1] FALSE
##
## $open_issues
## [1] 0
# make a data frame of that info for all a user's repos
map_df(one_user, `[`, c("name", "fork", "open_issues"))
## # A tibble: 30 x 3
## name fork open_issues
## <chr> <lgl> <int>
## 1 after FALSE 0
## 2 argufy FALSE 6
## 3 ask FALSE 4
## 4 baseimports FALSE 0
## 5 citest TRUE 0
## 6 clisymbols FALSE 0
## 7 cmaker TRUE 0
## 8 cmark TRUE 0
## 9 conditions TRUE 0
## 10 crayon FALSE 7
## # ... with 20 more rows
map_df(one_user, extract, c("name", "fork", "open_issues"))
## # A tibble: 30 x 3
## name fork open_issues
## <chr> <lgl> <int>
## 1 after FALSE 0
## 2 argufy FALSE 6
## 3 ask FALSE 4
## 4 baseimports FALSE 0
## 5 citest TRUE 0
## 6 clisymbols FALSE 0
## 7 cmaker TRUE 0
## 8 cmark TRUE 0
## 9 conditions TRUE 0
## 10 crayon FALSE 7
## # ... with 20 more rows
Next let’s scale this up to all the users in the data frame by executing a map()
inside of a map()
:
udf %>%
mutate(repo_info = gh_repos %>%
map(. %>%
map_df(extract, c("name", "fork", "open_issues"))))
## # A tibble: 6 x 3
## username gh_repos repo_info
## <chr> <list> <list>
## 1 gaborcsardi <list [30]> <tibble [30 x 3]>
## 2 jennybc <list [30]> <tibble [30 x 3]>
## 3 jtleek <list [30]> <tibble [30 x 3]>
## 4 juliasilge <list [26]> <tibble [26 x 3]>
## 5 leeper <list [30]> <tibble [30 x 3]>
## 6 masalmon <list [30]> <tibble [30 x 3]>
Now that we extracted our user-specific information, we want to make this a tidy data frame. All the info we want is in repo_info
, so we can remove gh_repos
and unnest()
the data frame:
(rdf <- udf %>%
mutate(
repo_info = gh_repos %>%
map(. %>%
map_df(extract, c("name", "fork", "open_issues")))
) %>%
select(-gh_repos) %>%
tidyr::unnest())
## # A tibble: 176 x 4
## username name fork open_issues
## <chr> <chr> <lgl> <int>
## 1 gaborcsardi after FALSE 0
## 2 gaborcsardi argufy FALSE 6
## 3 gaborcsardi ask FALSE 4
## 4 gaborcsardi baseimports FALSE 0
## 5 gaborcsardi citest TRUE 0
## 6 gaborcsardi clisymbols FALSE 0
## 7 gaborcsardi cmaker TRUE 0
## 8 gaborcsardi cmark TRUE 0
## 9 gaborcsardi conditions TRUE 0
## 10 gaborcsardi crayon FALSE 7
## # ... with 166 more rows
purrr
tutorialdevtools::session_info()
## Session info -------------------------------------------------------------
## setting value
## version R version 3.4.1 (2017-06-30)
## system x86_64, darwin15.6.0
## ui X11
## language (EN)
## collate en_US.UTF-8
## tz America/Chicago
## date 2017-11-10
## Packages -----------------------------------------------------------------
## package * version date source
## assertthat 0.2.0 2017-04-11 CRAN (R 3.4.0)
## backports 1.1.0 2017-05-22 CRAN (R 3.4.0)
## base * 3.4.1 2017-07-07 local
## bindr 0.1 2016-11-13 CRAN (R 3.4.0)
## bindrcpp * 0.2 2017-06-17 CRAN (R 3.4.0)
## boxes 0.0.0.9000 2017-07-19 Github (r-pkgs/boxes@03098dc)
## broom 0.4.2 2017-08-09 local
## cellranger 1.1.0 2016-07-27 CRAN (R 3.4.0)
## clisymbols 1.2.0 2017-05-21 cran (@1.2.0)
## codetools 0.2-15 2016-10-05 CRAN (R 3.4.1)
## colorspace 1.3-2 2016-12-14 CRAN (R 3.4.0)
## compiler 3.4.1 2017-07-07 local
## crayon 1.3.4 2017-10-03 Github (gaborcsardi/crayon@b5221ab)
## curl * 2.8.1 2017-07-21 CRAN (R 3.4.1)
## datasets * 3.4.1 2017-07-07 local
## devtools 1.13.3 2017-08-02 CRAN (R 3.4.1)
## digest 0.6.12 2017-01-27 CRAN (R 3.4.0)
## dplyr * 0.7.4.9000 2017-10-03 Github (tidyverse/dplyr@1a0730a)
## evaluate 0.10.1 2017-06-24 CRAN (R 3.4.1)
## forcats * 0.2.0 2017-01-23 CRAN (R 3.4.0)
## foreign 0.8-69 2017-06-22 CRAN (R 3.4.1)
## ggplot2 * 2.2.1 2016-12-30 CRAN (R 3.4.0)
## glue 1.1.1 2017-06-21 CRAN (R 3.4.1)
## graphics * 3.4.1 2017-07-07 local
## grDevices * 3.4.1 2017-07-07 local
## grid 3.4.1 2017-07-07 local
## gtable 0.2.0 2016-02-26 CRAN (R 3.4.0)
## haven 1.1.0 2017-07-09 CRAN (R 3.4.1)
## hms 0.3 2016-11-22 CRAN (R 3.4.0)
## htmltools 0.3.6 2017-04-28 CRAN (R 3.4.0)
## htmlwidgets 0.9 2017-07-10 CRAN (R 3.4.1)
## httr 1.3.1 2017-08-20 CRAN (R 3.4.1)
## jsonlite * 1.5 2017-06-01 CRAN (R 3.4.0)
## knitr 1.17 2017-08-10 cran (@1.17)
## lattice 0.20-35 2017-03-25 CRAN (R 3.4.1)
## lazyeval 0.2.0 2016-06-12 CRAN (R 3.4.0)
## listviewer * 1.4.0 2016-11-03 CRAN (R 3.4.0)
## lubridate 1.6.0 2016-09-13 CRAN (R 3.4.0)
## magrittr * 1.5 2014-11-22 CRAN (R 3.4.0)
## memoise 1.1.0 2017-04-21 CRAN (R 3.4.0)
## methods * 3.4.1 2017-07-07 local
## mnormt 1.5-5 2016-10-15 CRAN (R 3.4.0)
## modelr 0.1.1 2017-08-10 local
## munsell 0.4.3 2016-02-13 CRAN (R 3.4.0)
## nlme 3.1-131 2017-02-06 CRAN (R 3.4.1)
## parallel 3.4.1 2017-07-07 local
## pkgconfig 2.0.1 2017-03-21 CRAN (R 3.4.0)
## plyr 1.8.4 2016-06-08 CRAN (R 3.4.0)
## psych 1.7.5 2017-05-03 CRAN (R 3.4.1)
## purrr * 0.2.3 2017-08-02 CRAN (R 3.4.1)
## R6 2.2.2 2017-06-17 CRAN (R 3.4.0)
## Rcpp 0.12.13 2017-09-28 cran (@0.12.13)
## readr * 1.1.1 2017-05-16 CRAN (R 3.4.0)
## readxl 1.0.0 2017-04-18 CRAN (R 3.4.0)
## repurrrsive * 0.0.0.9001 2017-08-22 Github (jennybc/repurrrsive@bbc3727)
## reshape2 1.4.2 2016-10-22 CRAN (R 3.4.0)
## rlang 0.1.2 2017-08-09 CRAN (R 3.4.1)
## rmarkdown 1.6 2017-06-15 CRAN (R 3.4.0)
## rprojroot 1.2 2017-01-16 CRAN (R 3.4.0)
## rstudioapi 0.6 2016-06-27 CRAN (R 3.4.0)
## rvest 0.3.2 2016-06-17 CRAN (R 3.4.0)
## scales 0.4.1 2016-11-09 CRAN (R 3.4.0)
## stats * 3.4.1 2017-07-07 local
## stringi 1.1.5 2017-04-07 CRAN (R 3.4.0)
## stringr * 1.2.0 2017-02-18 CRAN (R 3.4.0)
## tibble * 1.3.4 2017-08-22 CRAN (R 3.4.1)
## tidyr * 0.7.0 2017-08-16 CRAN (R 3.4.1)
## tidyverse * 1.1.1.9000 2017-07-19 Github (tidyverse/tidyverse@a028619)
## tools 3.4.1 2017-07-07 local
## utils * 3.4.1 2017-07-07 local
## withr 2.0.0 2017-07-28 CRAN (R 3.4.1)
## xml2 1.1.1 2017-01-24 CRAN (R 3.4.0)
## yaml 2.1.14 2016-11-12 CRAN (R 3.4.0)
This work is licensed under the CC BY-NC 4.0 Creative Commons License.