List of vectors to list of lists
Asked Answered
Z

5

6

I have a list structured this way:

x <- list(id = c("a", "b"),
          value = c(1,2),
          othervalue = c(3,4)
    )

I need to transform the list to this structure like this:

y <- list(a = list(value = 1, othervalue = 3),
          b = list(value = 2, othervalue = 4)
    )

How would you do this ?


Edit:

I stumbled upon a more advanced version of this problem:

Here there is some kind of nested list in the output.

x <-  list(id = c("a", "b", "a"), key = c("foo", "foo", "bar"), value = c(1, 2, 3))

to

y <- list(a = list(foo = 1, bar = 2), b  = list(foo = 3))

With the current answers, the result is:

$a
$a$key
[1] "foo"

$a$value
[1] 1


$b
$b$key
[1] "foo"

$b$value
[1] 2


$a
$a$key
[1] "bar"

$a$value
[1] 3
Zygote answered 12/6, 2023 at 10:35 Comment(1)
Thanks all for the answers, I stumbled on an advanced version of this problem. Any idea how to solve this ?Zygote
C
7

You can use list in Map with do.call.

z <- setNames(do.call(Map, c(list, x[-1])), x[[1]])

identical(z, y)
#[1] TRUE

The same but using pipes:

z <- c(list, x[-1]) |>
       do.call(what=Map) |>
       setNames(x[[1]])

Benchmark

x <- list(id = c("a", "b"), value = c(1,2), othervalue = c(3,4) )

bench::mark(purr = purrr::transpose(x[-1], .names = x[[1]]), #@Maël
            lapplySplit = lapply(split(as.data.frame(x)[-1], x$id), c), #@Allan Cameron
            Map = setNames(do.call(Map, c(list, x[-1])), x[[1]]) ) #@GKi

# expression       min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#  <bch:expr>  <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#1 purr          2.51ms   2.64ms      372.    3.69MB     34.2   152    14
#2 lapplySplit 461.77µs 490.48µs     2018.  102.21KB     52.0   892    23
#3 Map          14.13µs   15.9µs    61557.    3.06KB     80.1  9987    13

Map is in this case about 30 times faster and allocates mutch less memory compared to lapplySplit the second.
And with the dataset from @s_baldur:

x <- list(id = c(letters, LETTERS), value = 1:52, othervalue = (1:52 + 100))
bench::mark(check=FALSE,
purr = purrr::transpose(x[-1], .names = x[[1]]), #@Maël
lapplySplit = lapply(split(as.data.frame(x)[-1], x$id), c), #@Allan Cameron
Map = setNames(do.call(Map, c(list, x[-1])), x[[1]]) ) #@GKi
#  expression       min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#  <bch:expr>  <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#1 purr           2.5ms   2.59ms      385.    3.69MB     33.9   159    14
#2 lapplySplit   2.43ms   2.51ms      391.  137.21KB     68.3   149    26
#3 Map          50.98µs  61.06µs    15717.    3.51KB     72.5  6506    30

For the updated question use maybe:

lapply(split(setNames(x$value, x$key), x$id), as.list)

But is there a need for a list? If not then use maybe:

split(setNames(x$value, x$key), x$id)
Carnal answered 12/6, 2023 at 10:41 Comment(3)
transpose is actually much faster on bigger data. see @s_baldur's solution.Eisenhower
@Maël actually on my machine it's also much faster on the small data. Using purrr 0.3.4Varese
@Maël I have add a Benchmark with the other dataset, but the ranking is on my pc different. Maybe s_baldur is using another R version? I use 4.3.0. Or different libraries or hardware. Can you confirm s_baldur ranking or mine or have totally different?Carnal
L
5

Since you're mentioning purrr in your tags, you can use purrr::transpose:

purrr::transpose(x[-1], .names = x[[1]])

# $a
# $a$value
# [1] 1
# 
# $a$othervalue
# [1] 3
# 
# 
# $b
# $b$value
# [1] 2
# 
# $b$othervalue
# [1] 4

Your second question looks like recursive splitting. To do so, a convenient option is collapse::rsplit:

collapse::rsplit(data.frame(x), ~ id + key)

# $a
# $a$bar
# [1] 3
# 
# $a$foo
# [1] 1
# 
# 
# $b
# $b$foo
# [1] 2
Lamebrain answered 12/6, 2023 at 10:58 Comment(0)
C
4

You could do

lapply(split(as.data.frame(x)[-1], x$id), c)
#> $a
#> $a$value
#> [1] 1
#> 
#> $a$othervalue
#> [1] 3
#> 
#> 
#> $b
#> $b$value
#> [1] 2
#> 
#> $b$othervalue
#> [1] 4

Which is identical to y:

identical(lapply(split(as.data.frame(x)[-1], x$id), c), y)
#> [1] TRUE
Casandracasanova answered 12/6, 2023 at 10:39 Comment(1)
This one might not respect the original order in x$id.Varese
V
3

Keeping it simple:

foo <- function(x) {
  n <- length(x$id)
  y <- vector(mode = "list", length = n) |> setNames(x$id)
  for (i in seq_len(n)) y[[i]] <- list(value = x$value[i], othervalue = x$othervalue[i])
  y
}

Benchmark (with slightly bigger data):

x <- list(id = c(letters, LETTERS),
          value = 1:52,
          othervalue = (1:52 + 100)
    )
bench::mark(purr = purrr::transpose(x[-1], .names = x[[1]]),
            Map = setNames(do.call(Map, c(list, x[-1])), x[[1]]),
            loop = foo(x)) 

#   expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time                 
#   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>            
# 1 purr            5µs    5.8µs   137969.     1.2KB     27.6  9998     2     72.5ms
# 2 Map          32.3µs   34.7µs    27567.      464B     13.8  9995     5    362.6ms
# 3 loop         16.5µs   17.5µs    52962.      464B     15.9  9997     3    188.8ms 
Varese answered 12/6, 2023 at 11:24 Comment(0)
M
2

Update

Regarding the update in the question, you can try split + lapply

lapply(split(list2DF(x[-1]), x[[1]]), \(v) with(v, split(value, key)))

or we can use aggregate + Map

with(
    aggregate(x[-1], x[1], as.list),
    setNames(Map(setNames, value, key), id)
)

which gives

$a
$a$bar
[1] 3

$a$foo
[1] 1


$b
$b$foo
[1] 2

For Previous Question

You can try

list2DF(x[-1]) %>%
    split(1:nrow(.)) %>%
    setNames(x[[1]]) %>%
    lapply(c)

which gives

$a
$a$value
[1] 1

$a$othervalue
[1] 3


$b
$b$value
[1] 2

$b$othervalue
[1] 4
Mafala answered 12/6, 2023 at 12:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.