17 min read

Purrr´s map and friends - good practise

This post is my personal reference for functional programming with purrr package. I often find myself to search on google/stackoverflow “how to” iterate over this and that so it make sense to put it together somewhere. This post is vastly based on brilliant Jenny Bryan´s purrr tutorial and focus on nested lists / list-columns.

library(repurrrsive)
library(tidyverse)

Two datasets from repurrrsive package to practise on:

  • got_char Game of Thrones characters dataset.
  • gh_repos Github repositories dataset.

Start simple

  • using str and it´s max.level and list.len argument
  • using View() function in RStudio
# is it even list?
got_chars %>% class
## [1] "list"
# list.len limits each nesting level to x items (by default list.len = 99)
# in case of more nested lists use max.level argument, by default all levels are shown
got_chars %>% str(list.len = 3)
## List of 30
##  $ :List of 18
##   ..$ url        : chr "https://www.anapioficeandfire.com/api/characters/1022"
##   ..$ id         : int 1022
##   ..$ name       : chr "Theon Greyjoy"
##   .. [list output truncated]
##  $ :List of 18
##   ..$ url        : chr "https://www.anapioficeandfire.com/api/characters/1052"
##   ..$ id         : int 1052
##   ..$ name       : chr "Tyrion Lannister"
##   .. [list output truncated]
##  $ :List of 18
##   ..$ url        : chr "https://www.anapioficeandfire.com/api/characters/1074"
##   ..$ id         : int 1074
##   ..$ name       : chr "Victarion Greyjoy"
##   .. [list output truncated]
##   [list output truncated]

So got_char is a list of 30 lists, each with 18 items.

We can get this information this way too:

# number of items (lists in this case) at level 0 
got_chars %>% length()
## [1] 30
# number of items at level 1
got_chars %>% map_int(length)
##  [1] 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18
## [24] 18 18 18 18 18 18 18

Or using View() function in RStudio. I prefer it.

..down to 30 lists

Now I want all items just from first list:

got_chars[1] %>% str
## List of 1
##  $ :List of 18
##   ..$ url        : chr "https://www.anapioficeandfire.com/api/characters/1022"
##   ..$ id         : int 1022
##   ..$ name       : chr "Theon Greyjoy"
##   ..$ gender     : chr "Male"
##   ..$ culture    : chr "Ironborn"
##   ..$ born       : chr "In 278 AC or 279 AC, at Pyke"
##   ..$ died       : chr ""
##   ..$ alive      : logi TRUE
##   ..$ titles     : chr [1:3] "Prince of Winterfell" "Captain of Sea Bitch" "Lord of the Iron Islands (by law of the green lands)"
##   ..$ aliases    : chr [1:4] "Prince of Fools" "Theon Turncloak" "Reek" "Theon Kinslayer"
##   ..$ father     : chr ""
##   ..$ mother     : chr ""
##   ..$ spouse     : chr ""
##   ..$ allegiances: chr "House Greyjoy of Pyke"
##   ..$ books      : chr [1:3] "A Game of Thrones" "A Storm of Swords" "A Feast for Crows"
##   ..$ povBooks   : chr [1:2] "A Clash of Kings" "A Dance with Dragons"
##   ..$ tvSeries   : chr [1:6] "Season 1" "Season 2" "Season 3" "Season 4" ...
##   ..$ playedBy   : chr "Alfie Allen"

Here is good to realize:
a) that single [ returns always list
b) even if 5th element is pulled, the index of outcome will be 1, not 5.

got_chars[5]
## [[1]]
## [[1]]$url
## [1] "https://www.anapioficeandfire.com/api/characters/1166"
## 
## [[1]]$id
## [1] 1166
## 
## [[1]]$name
## [1] "Areo Hotah"
## 
## [[1]]$gender
## [1] "Male"
## 
## [[1]]$culture
## [1] "Norvoshi"
## 
## [[1]]$born
## [1] "In 257 AC or before, at Norvos"
## 
## [[1]]$died
## [1] ""
## 
## [[1]]$alive
## [1] TRUE
## 
## [[1]]$titles
## [1] "Captain of the Guard at Sunspear"
## 
## [[1]]$aliases
## [1] ""
## 
## [[1]]$father
## [1] ""
## 
## [[1]]$mother
## [1] ""
## 
## [[1]]$spouse
## [1] ""
## 
## [[1]]$allegiances
## [1] "House Nymeros Martell of Sunspear"
## 
## [[1]]$books
## [1] "A Game of Thrones" "A Clash of Kings"  "A Storm of Swords"
## 
## [[1]]$povBooks
## [1] "A Feast for Crows"    "A Dance with Dragons"
## 
## [[1]]$tvSeries
## [1] "Season 5" "Season 6"
## 
## [[1]]$playedBy
## [1] "DeObia Oparei"

Now, I want all content of items just from first list:

# outcome is a list again but with one level only
got_chars[[1]] %>% str
## List of 18
##  $ url        : chr "https://www.anapioficeandfire.com/api/characters/1022"
##  $ id         : int 1022
##  $ name       : chr "Theon Greyjoy"
##  $ gender     : chr "Male"
##  $ culture    : chr "Ironborn"
##  $ born       : chr "In 278 AC or 279 AC, at Pyke"
##  $ died       : chr ""
##  $ alive      : logi TRUE
##  $ titles     : chr [1:3] "Prince of Winterfell" "Captain of Sea Bitch" "Lord of the Iron Islands (by law of the green lands)"
##  $ aliases    : chr [1:4] "Prince of Fools" "Theon Turncloak" "Reek" "Theon Kinslayer"
##  $ father     : chr ""
##  $ mother     : chr ""
##  $ spouse     : chr ""
##  $ allegiances: chr "House Greyjoy of Pyke"
##  $ books      : chr [1:3] "A Game of Thrones" "A Storm of Swords" "A Feast for Crows"
##  $ povBooks   : chr [1:2] "A Clash of Kings" "A Dance with Dragons"
##  $ tvSeries   : chr [1:6] "Season 1" "Season 2" "Season 3" "Season 4" ...
##  $ playedBy   : chr "Alfie Allen"

Good to realize:

got_chars[1] %>% names
## NULL

vs.

got_chars[[1]] %>% names
##  [1] "url"         "id"          "name"        "gender"      "culture"    
##  [6] "born"        "died"        "alive"       "titles"      "aliases"    
## [11] "father"      "mother"      "spouse"      "allegiances" "books"      
## [16] "povBooks"    "tvSeries"    "playedBy"

Extract name from each list:

got_chars %>% map_chr(~.$name)
##  [1] "Theon Greyjoy"      "Tyrion Lannister"   "Victarion Greyjoy" 
##  [4] "Will"               "Areo Hotah"         "Chett"             
##  [7] "Cressen"            "Arianne Martell"    "Daenerys Targaryen"
## [10] "Davos Seaworth"     "Arya Stark"         "Arys Oakheart"     
## [13] "Asha Greyjoy"       "Barristan Selmy"    "Varamyr"           
## [16] "Brandon Stark"      "Brienne of Tarth"   "Catelyn Stark"     
## [19] "Cersei Lannister"   "Eddard Stark"       "Jaime Lannister"   
## [22] "Jon Connington"     "Jon Snow"           "Aeron Greyjoy"     
## [25] "Kevan Lannister"    "Melisandre"         "Merrett Frey"      
## [28] "Quentyn Martell"    "Samwell Tarly"      "Sansa Stark"

or

got_chars %>% map_chr(~.x$name)
##  [1] "Theon Greyjoy"      "Tyrion Lannister"   "Victarion Greyjoy" 
##  [4] "Will"               "Areo Hotah"         "Chett"             
##  [7] "Cressen"            "Arianne Martell"    "Daenerys Targaryen"
## [10] "Davos Seaworth"     "Arya Stark"         "Arys Oakheart"     
## [13] "Asha Greyjoy"       "Barristan Selmy"    "Varamyr"           
## [16] "Brandon Stark"      "Brienne of Tarth"   "Catelyn Stark"     
## [19] "Cersei Lannister"   "Eddard Stark"       "Jaime Lannister"   
## [22] "Jon Connington"     "Jon Snow"           "Aeron Greyjoy"     
## [25] "Kevan Lannister"    "Melisandre"         "Merrett Frey"      
## [28] "Quentyn Martell"    "Samwell Tarly"      "Sansa Stark"

or

got_chars %>% map_chr("name")
##  [1] "Theon Greyjoy"      "Tyrion Lannister"   "Victarion Greyjoy" 
##  [4] "Will"               "Areo Hotah"         "Chett"             
##  [7] "Cressen"            "Arianne Martell"    "Daenerys Targaryen"
## [10] "Davos Seaworth"     "Arya Stark"         "Arys Oakheart"     
## [13] "Asha Greyjoy"       "Barristan Selmy"    "Varamyr"           
## [16] "Brandon Stark"      "Brienne of Tarth"   "Catelyn Stark"     
## [19] "Cersei Lannister"   "Eddard Stark"       "Jaime Lannister"   
## [22] "Jon Connington"     "Jon Snow"           "Aeron Greyjoy"     
## [25] "Kevan Lannister"    "Melisandre"         "Merrett Frey"      
## [28] "Quentyn Martell"    "Samwell Tarly"      "Sansa Stark"

or straight to tibble:

got_chars %>% map_df(~.[c("name", "gender")])
## # A tibble: 30 x 2
##    name               gender
##    <chr>              <chr> 
##  1 Theon Greyjoy      Male  
##  2 Tyrion Lannister   Male  
##  3 Victarion Greyjoy  Male  
##  4 Will               Male  
##  5 Areo Hotah         Male  
##  6 Chett              Male  
##  7 Cressen            Male  
##  8 Arianne Martell    Female
##  9 Daenerys Targaryen Female
## 10 Davos Seaworth     Male  
## # ... with 20 more rows

What if I want “aliases” too?

got_chars %>% map_df(~.[c("name", "gender", "aliases")])
## Error: Argument 3 must be length 1, not 4

..it doesn´t work beacause some characters have more than one aliase.

But this does:

# alias will be list-colum ("nested")
df <- tibble(
  name = got_chars %>% map_chr("name"),
  id = got_chars %>% map_int("id"),
  gender = got_chars %>% map_chr("gender"),
  aliases = got_chars %>% map("aliases")
)
df
## # A tibble: 30 x 4
##    name                  id gender aliases   
##    <chr>              <int> <chr>  <list>    
##  1 Theon Greyjoy       1022 Male   <chr [4]> 
##  2 Tyrion Lannister    1052 Male   <chr [11]>
##  3 Victarion Greyjoy   1074 Male   <chr [1]> 
##  4 Will                1109 Male   <chr [1]> 
##  5 Areo Hotah          1166 Male   <chr [1]> 
##  6 Chett               1267 Male   <chr [1]> 
##  7 Cressen             1295 Male   <chr [1]> 
##  8 Arianne Martell      130 Female <chr [1]> 
##  9 Daenerys Targaryen  1303 Female <chr [11]>
## 10 Davos Seaworth      1319 Male   <chr [5]> 
## # ... with 20 more rows

..it works because alias is bind as list-colum (“nested”)

Using unnest comes handy now? Nope yet..

df %>% 
  unnest()
## Warning: `cols` is now required.
## Please use `cols = c(aliases)`
## # A tibble: 114 x 4
##    name                id gender aliases           
##    <chr>            <int> <chr>  <chr>             
##  1 Theon Greyjoy     1022 Male   Prince of Fools   
##  2 Theon Greyjoy     1022 Male   Theon Turncloak   
##  3 Theon Greyjoy     1022 Male   Reek              
##  4 Theon Greyjoy     1022 Male   Theon Kinslayer   
##  5 Tyrion Lannister  1052 Male   The Imp           
##  6 Tyrion Lannister  1052 Male   Halfman           
##  7 Tyrion Lannister  1052 Male   The boyman        
##  8 Tyrion Lannister  1052 Male   Giant of Lannister
##  9 Tyrion Lannister  1052 Male   Lord Tywin's Doom 
## 10 Tyrion Lannister  1052 Male   Lord Tywin's Bane 
## # ... with 104 more rows

..it is because some of alias has NA (NULL) value (# NULL are the list-col equivalent of NAs):

df$aliases %>% map_lgl(is.null)
##  [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [12] FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE
## [23] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE

We can use replace_na to replace NULL to "" or whatever and unnest afterwards.

df %>% 
  replace_na(list(aliases=list(""))) %>% 
  unnest()
## Warning: `cols` is now required.
## Please use `cols = c(aliases)`
## # A tibble: 115 x 4
##    name                id gender aliases           
##    <chr>            <int> <chr>  <chr>             
##  1 Theon Greyjoy     1022 Male   Prince of Fools   
##  2 Theon Greyjoy     1022 Male   Theon Turncloak   
##  3 Theon Greyjoy     1022 Male   Reek              
##  4 Theon Greyjoy     1022 Male   Theon Kinslayer   
##  5 Tyrion Lannister  1052 Male   The Imp           
##  6 Tyrion Lannister  1052 Male   Halfman           
##  7 Tyrion Lannister  1052 Male   The boyman        
##  8 Tyrion Lannister  1052 Male   Giant of Lannister
##  9 Tyrion Lannister  1052 Male   Lord Tywin's Doom 
## 10 Tyrion Lannister  1052 Male   Lord Tywin's Bane 
## # ... with 105 more rows

To tibble asap

..extracting using mutate+map+unnest
..allows iterative approach - more decomposed code and faster debugging

a) directly to tibble

tibble(name = got_chars %>% map_chr(~.$name)) %>% 
  mutate(id =  got_chars %>% map_int(~.$id)) %>% 
  mutate(aliases =  got_chars %>% map(~.$aliases)) %>%
  replace_na(list(aliases = list(""))) %>% 
  unnest(aliases)
## # A tibble: 115 x 3
##    name                id aliases           
##    <chr>            <int> <chr>             
##  1 Theon Greyjoy     1022 Prince of Fools   
##  2 Theon Greyjoy     1022 Theon Turncloak   
##  3 Theon Greyjoy     1022 Reek              
##  4 Theon Greyjoy     1022 Theon Kinslayer   
##  5 Tyrion Lannister  1052 The Imp           
##  6 Tyrion Lannister  1052 Halfman           
##  7 Tyrion Lannister  1052 The boyman        
##  8 Tyrion Lannister  1052 Giant of Lannister
##  9 Tyrion Lannister  1052 Lord Tywin's Doom 
## 10 Tyrion Lannister  1052 Lord Tywin's Bane 
## # ... with 105 more rows

b) set_names and enframe the list (in fact it is directly to tibble too)

got_chars %>% 
  set_names(., got_chars %>% map_chr(~.$name)) %>% 
  enframe(name = "name", value = "data") %>% 
  mutate(id = data %>% map_int(~.$id)) %>% 
  # or
  mutate(id_2 = data %>% map(~.$id) %>% unlist) %>% 
  mutate(aliases =  data %>% map(~.$aliases)) %>%
  replace_na(list(aliases = list(""))) %>% 
  unnest(aliases, .drop = FALSE)
## Warning: The `.drop` argument of `unnest()` is deprecated as of tidyr 1.0.0.
## All list-columns are now preserved.
## This warning is displayed once per session.
## Call `lifecycle::last_warnings()` to see where this warning was generated.
## # A tibble: 115 x 5
##    name             data                 id  id_2 aliases           
##    <chr>            <list>            <int> <int> <chr>             
##  1 Theon Greyjoy    <named list [18]>  1022  1022 Prince of Fools   
##  2 Theon Greyjoy    <named list [18]>  1022  1022 Theon Turncloak   
##  3 Theon Greyjoy    <named list [18]>  1022  1022 Reek              
##  4 Theon Greyjoy    <named list [18]>  1022  1022 Theon Kinslayer   
##  5 Tyrion Lannister <named list [18]>  1052  1052 The Imp           
##  6 Tyrion Lannister <named list [18]>  1052  1052 Halfman           
##  7 Tyrion Lannister <named list [18]>  1052  1052 The boyman        
##  8 Tyrion Lannister <named list [18]>  1052  1052 Giant of Lannister
##  9 Tyrion Lannister <named list [18]>  1052  1052 Lord Tywin's Doom 
## 10 Tyrion Lannister <named list [18]>  1052  1052 Lord Tywin's Bane 
## # ... with 105 more rows

More nested stuff

# explore gh_repos list
gh_repos[[1]] %>% str(max.level = 4, list.len = 4)
## List of 30
##  $ :List of 68
##   ..$ id               : int 61160198
##   ..$ name             : chr "after"
##   ..$ full_name        : chr "gaborcsardi/after"
##   ..$ owner            :List of 17
##   .. ..$ login              : chr "gaborcsardi"
##   .. ..$ id                 : int 660288
##   .. ..$ avatar_url         : chr "https://avatars.githubusercontent.com/u/660288?v=3"
##   .. ..$ gravatar_id        : chr ""
##   .. .. [list output truncated]
##   .. [list output truncated]
##  $ :List of 68
##   ..$ id               : int 40500181
##   ..$ name             : chr "argufy"
##   ..$ full_name        : chr "gaborcsardi/argufy"
##   ..$ owner            :List of 17
##   .. ..$ login              : chr "gaborcsardi"
##   .. ..$ id                 : int 660288
##   .. ..$ avatar_url         : chr "https://avatars.githubusercontent.com/u/660288?v=3"
##   .. ..$ gravatar_id        : chr ""
##   .. .. [list output truncated]
##   .. [list output truncated]
##  $ :List of 68
##   ..$ id               : int 36442442
##   ..$ name             : chr "ask"
##   ..$ full_name        : chr "gaborcsardi/ask"
##   ..$ owner            :List of 17
##   .. ..$ login              : chr "gaborcsardi"
##   .. ..$ id                 : int 660288
##   .. ..$ avatar_url         : chr "https://avatars.githubusercontent.com/u/660288?v=3"
##   .. ..$ gravatar_id        : chr ""
##   .. .. [list output truncated]
##   .. [list output truncated]
##  $ :List of 68
##   ..$ id               : int 34924886
##   ..$ name             : chr "baseimports"
##   ..$ full_name        : chr "gaborcsardi/baseimports"
##   ..$ owner            :List of 17
##   .. ..$ login              : chr "gaborcsardi"
##   .. ..$ id                 : int 660288
##   .. ..$ avatar_url         : chr "https://avatars.githubusercontent.com/u/660288?v=3"
##   .. ..$ gravatar_id        : chr ""
##   .. .. [list output truncated]
##   .. [list output truncated]
##   [list output truncated]

or again using View() function in RStudio. I prefer it because you can easily miss something when using str (yes, it sounds strange, but that is my experiance with complicated lists). There are two major advantages:

  1. Using View you can explore single element down to latest element without extending the list with other elements (unlike str)
  2. Using View you can get direct address to now-matter-how-much-scary-nested element (can be done with str too actually, but more complicated way)

or

# number of item at level 0
gh_repos %>% length()
## [1] 6
# number of item at level 1
gh_repos %>% map_int(length)
## [1] 30 30 30 26 30 30
# number of item at level 2
gh_repos %>% map(map_int, length)
## [[1]]
##  [1] 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68
## [24] 68 68 68 68 68 68 68
## 
## [[2]]
##  [1] 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68
## [24] 68 68 68 68 68 68 68
## 
## [[3]]
##  [1] 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68
## [24] 68 68 68 68 68 68 68
## 
## [[4]]
##  [1] 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68
## [24] 68 68 68
## 
## [[5]]
##  [1] 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68
## [24] 68 68 68 68 68 68 68
## 
## [[6]]
##  [1] 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68
## [24] 68 68 68 68 68 68 68

..more about last one later
..gh_repos is list of 6 lists, each with 30 (except one with 26 lists), each with 68 items

# exctract name
gh_repos %>% map(~.[[1]][["name"]])
## [[1]]
## [1] "after"
## 
## [[2]]
## [1] "2013-11_sfu"
## 
## [[3]]
## [1] "advdatasci"
## 
## [[4]]
## [1] "2016-14"
## 
## [[5]]
## [1] "ampolcourse"
## 
## [[6]]
## [1] "aqi_pdf"

..but it is NOT correct. Why? Because this is just item “name” from each first sub-list. We need somehow iterate over index map_chr(~.[[i]][[“name”]])

gh_repos %>% map(function(x){map_chr(x, ~.$name)
  }) %>% 
  str()
## List of 6
##  $ : chr [1:30] "after" "argufy" "ask" "baseimports" ...
##  $ : chr [1:30] "2013-11_sfu" "2014-01-27-miami" "2014-05-12-ubc" "2015-02-23_bryan-fields-talk" ...
##  $ : chr [1:30] "advdatasci" "advdatasci-swirl" "advdatasci16" "advdatasci_swirl" ...
##  $ : chr [1:26] "2016-14" "choroplethrCaCensusTract" "choroplethrUTCensusTract" "CountyHealthApp" ...
##  $ : chr [1:30] "ampolcourse" "apsa-leeper.bst" "arco" "astrojs" ...
##  $ : chr [1:30] "aqi_pdf" "catan_card_game" "colourlovers_patterns" "convertagd" ...

That´s it!
or

gh_repos %>% map(map_chr, ~.$name) %>% 
  str()
## List of 6
##  $ : chr [1:30] "after" "argufy" "ask" "baseimports" ...
##  $ : chr [1:30] "2013-11_sfu" "2014-01-27-miami" "2014-05-12-ubc" "2015-02-23_bryan-fields-talk" ...
##  $ : chr [1:30] "advdatasci" "advdatasci-swirl" "advdatasci16" "advdatasci_swirl" ...
##  $ : chr [1:26] "2016-14" "choroplethrCaCensusTract" "choroplethrUTCensusTract" "CountyHealthApp" ...
##  $ : chr [1:30] "ampolcourse" "apsa-leeper.bst" "arco" "astrojs" ...
##  $ : chr [1:30] "aqi_pdf" "catan_card_game" "colourlovers_patterns" "convertagd" ...

That´s it again.
or

gh_repos %>% modify_depth(2, ~.$name) %>% 
  str(list.len = 4)
## List of 6
##  $ :List of 30
##   ..$ : chr "after"
##   ..$ : chr "argufy"
##   ..$ : chr "ask"
##   ..$ : chr "baseimports"
##   .. [list output truncated]
##  $ :List of 30
##   ..$ : chr "2013-11_sfu"
##   ..$ : chr "2014-01-27-miami"
##   ..$ : chr "2014-05-12-ubc"
##   ..$ : chr "2015-02-23_bryan-fields-talk"
##   .. [list output truncated]
##  $ :List of 30
##   ..$ : chr "advdatasci"
##   ..$ : chr "advdatasci-swirl"
##   ..$ : chr "advdatasci16"
##   ..$ : chr "advdatasci_swirl"
##   .. [list output truncated]
##  $ :List of 26
##   ..$ : chr "2016-14"
##   ..$ : chr "choroplethrCaCensusTract"
##   ..$ : chr "choroplethrUTCensusTract"
##   ..$ : chr "CountyHealthApp"
##   .. [list output truncated]
##   [list output truncated]
# modify_depth is a little bit difficult to abstract and the disadvantage is that it return list only, there isn´t modify_depth_char equivalent

Now, we will extract owner list, more specifically login variable. We can notice it is duplicated in each list = useful to set as name. The index = 1 is OK as we will extract just one (first) item from list as all others are the same.

# extract login
gh_repos %>% map_chr(~.[[1]][["owner"]][["login"]])
## [1] "gaborcsardi" "jennybc"     "jtleek"      "juliasilge"  "leeper"     
## [6] "masalmon"

or

# extract login
gh_repos %>% map_chr(~.[[1]]$owner$login)
## [1] "gaborcsardi" "jennybc"     "jtleek"      "juliasilge"  "leeper"     
## [6] "masalmon"

or

gh_repos %>% map_chr(c(1,4,1))
## [1] "gaborcsardi" "jennybc"     "jtleek"      "juliasilge"  "leeper"     
## [6] "masalmon"

The best practise - put it all together

# explore the list first with View() or str, understand the data
# put list into tibble using enframe (list must have names) or set_names first = get the address to logical element for your names
gh_repos %>%
  set_names(., map_chr(gh_repos, ~.[[1]]$owner$login)) %>% 
  enframe(name = "name", value = "data") -> t 
t
## # A tibble: 6 x 2
##   name        data       
##   <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]>

..here is good to realize that data is basically replicated gh_repos.

# now mutate whatever you want by extracting using mutate + map depending on level of nest
t %>% 
  mutate(package = data %>% map(map_chr, ~.$name)) %>%
  # the index = 1 is OK here as we will extract just one (first) item from sub-list as all others are the same.
  mutate(owner_url = data %>% map_chr(~.[[1]]$owner$url)) %>% 
  unnest(package, .drop = FALSE)
## # A tibble: 176 x 4
##    name        data        package    owner_url                            
##    <chr>       <list>      <chr>      <chr>                                
##  1 gaborcsardi <list [30]> after      https://api.github.com/users/gaborcs~
##  2 gaborcsardi <list [30]> argufy     https://api.github.com/users/gaborcs~
##  3 gaborcsardi <list [30]> ask        https://api.github.com/users/gaborcs~
##  4 gaborcsardi <list [30]> baseimpor~ https://api.github.com/users/gaborcs~
##  5 gaborcsardi <list [30]> citest     https://api.github.com/users/gaborcs~
##  6 gaborcsardi <list [30]> clisymbols https://api.github.com/users/gaborcs~
##  7 gaborcsardi <list [30]> cmaker     https://api.github.com/users/gaborcs~
##  8 gaborcsardi <list [30]> cmark      https://api.github.com/users/gaborcs~
##  9 gaborcsardi <list [30]> conditions https://api.github.com/users/gaborcs~
## 10 gaborcsardi <list [30]> crayon     https://api.github.com/users/gaborcs~
## # ... with 166 more rows