Modify a nested list using `list_modify` by position

purrr

#1

Hi community,

I'd like to modify the first value (numeric) of a nested list in a tibble by adding another numeric variable. I need to do this by position as the list elements have different names in different rows.

My investigations so far have led me to believe list_modify is the function that will get me there, but I can't figure out how to modify by list position rather than list name. The list elements have different names and lengths, which is why I'd like to modify by position.

Looking at the vignettes on the tidyverse site and in R documentation, the examples seem to me to only call by name, not by position.

Here's an explanatory tibble and examples of various broken, confused, gibberish code that I tried that failed.

In this example, I'd like to create a new variable par_adj where the first element of each par list is tweaked by adding the adj variable.

# Initialise a test tibble.

(test_tibble <- tibble(
  dist = c("norm", "exp"),
  par = list(
    list(mean = 1, sd = 1),
    list(rate = 3)
  ),
  adj = rep(2, 2)
))

# I wish to add the parameter adjustment adj to the first element of the par list. As the par list contains values with different names, I'd like to add adj to the first element of par by the first position. 

# This doesn't work, and I'm not really sure how to use the ..n call or what it means, but I saw something about it in .
# test_tibble %>% 
#   mutate(par_adj = list_modify(par, ..1 = list(map_dbl(par, 1) + adj)))

# Perhaps I need to map first. Not sure how tell list_modify to change the first value.
# test_tibble %>% 
#   mutate(par_adj = map(
#     par,
#     list_modify,
#     1,
#     ~1 + adj
#   ))

# Nope, this doesn't work either. 
# test_tibble %>% 
#   mutate(par_adj = map(
#     par,
#     list_modify,
#     1,
#     list(map_dbl(par, 1) + adj)
#   ))

Cheers


#2

I've never used the list_modify() function, so I can't help there, sorry.

But you could do this with an anonymous function in a "normal" map(). You'll also need to include your adj variable in the map(), i.e. use map2(). E.g. this seems to produce what I understood your desired result to be:

test_tibble %>% 
    mutate(
        par = map2(par, adj, function(list, adjustment) {
            list[[1]] <- list[[1]] + adjustment
            list
        })
    )

We can specify an anonymous function inside map() as shown, and then use "standard" list sub-settting to modify the first element of the list. We also need to pass your adj variable as an argument to the function in map2(), just calling adj from inside a map() as you had done was causing the whole field to be pulled in, and then R was recycling the arguments to ensure the lengths matched, not producing what I think you're trying to achieve.


#3

Seeing as I'm Sweden, tack tack (thank you), @jim89! :cake:

Much appreciated. Would still like to learn to use list_modify by position, but this did the trick. I was bamboozled by how my parameter lists are ragged length.

:notes:


#4

P. S. This is where I fixed it, quite the view.


#5

You can also do that with modify_at from the modify_* familly function in purrr :package:

library(dplyr, warn.conflicts = FALSE)
library(purrr)
(test_tibble <- tibble(
  dist = c("norm", "exp"),
  par = list(
    list(mean = 1, sd = 1),
    list(rate = 3)
  ),
  adj = rep(2, 2)
))
#> # A tibble: 2 x 3
#>   dist  par          adj
#>   <chr> <list>     <dbl>
#> 1 norm  <list [2]>     2
#> 2 exp   <list [1]>     2

test_tibble %>%
  mutate(par_adj = modify_at(par, 1, ~ map2(.x, adj, `+`)))
#> Warning: le package 'bindrcpp' a été compilé avec la version R 3.4.4
#> # A tibble: 2 x 4
#>   dist  par          adj par_adj   
#>   <chr> <list>     <dbl> <list>    
#> 1 norm  <list [2]>     2 <list [2]>
#> 2 exp   <list [1]>     2 <list [1]>

Created on 2018-05-01 by the reprex package (v0.2.0).


#6

I like these questions about the lesser known purrr functions to see some real examples in action. modify_at and list_modify are both new to me. @cderv, I looked at your example with interest because of this. But when comparing answers I couldn't match the modify_at result with the map2 only approach.

Reprex using slightly more minimal test_tibble:

library(tidyverse) 
test_tibble <- tibble(
  par = list(
    list(mean = 1, sd = 1),
    list(rate = 3)),
  adj = rep(2, 2))

test_tibble %>% glimpse()
#> Observations: 2
#> Variables: 2
#> $ par <list> [[1, 1], [3]]
#> $ adj <dbl> 2, 2

Using jim89's previous map2 only approach:

# using map2 only
test_tibble %>% 
  mutate(
    par = map2(par, adj, function(list, adjustment) {
      list[[1]] <- list[[1]] + adjustment
      list 
    })) %>% glimpse()
#> Observations: 2
#> Variables: 2
#> $ par <list> [[3, 1], [5]]
#> $ adj <dbl> 2, 2

Existing modify_at answer is different (+2 added to both elements of test_tibble$par[[1]])

test_tibble %>%
  mutate(par = modify_at(par, 1, ~ map2(.x, adj, `+`))) %>% 
  glimpse()
#> Observations: 2
#> Variables: 2
#> $ par <list> [[3, 3], [3]]
#> $ adj <dbl> 2, 2

Does the modify_at example needs rearranging slightly? Perhaps something like this?

test_tibble %>%
  mutate(par = map2(par, adj, function(a, b) { 
    modify_at(a, 1, ~ .x + .y, b) 
  })) %>% glimpse()
#> Observations: 2
#> Variables: 2
#> $ par <list> [[3, 1], [5]]
#> $ adj <dbl> 2, 2

Created on 2018-05-02 by the reprex package (v0.2.0).


#7

I misunderstood the question then. :thinking: I thought it was adding adj to the first element of par, but it is not. You're right. Let's say it is not the correct answer but an example of mutate_at. :wink:
Thanks for seeing it and add a way around. (nice idea glimpse to see list column.)