将分组的tibble转换为命名列表

9

我觉得在tidyverse中可能有比使用for-loop更好的方法。从一个标准的tibble/dataframe开始,创建一个列表,其中列表元素的名称是一列(group_by?)的唯一值,而列表元素则是另一列的所有值。

  my_data <- tibble(list_names = c("Ford", "Chevy", "Ford", "Dodge", "Dodge", "Ford"),
                    list_values = c("Ranger", "Equinox", "F150", "Caravan", "Ram", "Explorer"))
  
# A tibble: 6 × 2
  list_names list_values
  <chr>      <chr>      
1 Ford       Ranger     
2 Chevy      Equinox    
3 Ford       F150       
4 Dodge      Caravan    
5 Dodge      Ram        
6 Ford       Explorer

这是期望输出:

  desired_output <- list(Ford = c("Ranger", "F150", "Explorer"),
       Chevy = c("Equinox"),
       Dodge = c("Caravan", "Ram"))

$Ford
[1] "Ranger"   "F150"     "Explorer"

$Chevy
[1] "Equinox"

$Dodge
[1] "Caravan" "Ram" 

这可以通过 for-loop 实现,但我敢打赌有一个 tidyverse 函数可以使它更简单/更快,等等。

  desired_output <- list()
  for(i in seq_along(my_data$list_names)) {
    entry <- my_data %>%
      filter(list_names == my_data$list_names[i]) %>%
      pull(list_values)
    desired_output[[my_data$list_names[i]]] <- entry
  }
6个回答

7

这里还有另一种选项(虽然有点冗长),使用 tidyverse 中的 group_modifydeframe

library(tidyverse)

my_data |>
  group_by(list_names)  |>
  group_modify(\(x, ...) tibble(res = list(deframe(x)))) |>
  deframe()

另一个选择是使用summarise,然后再使用deframe

my_data %>%
  group_by(list_names) %>%
  summarise(named_vec = list(list_values)) %>%
  deframe()

输出

$Chevy
[1] "Equinox"

$Dodge
[1] "Caravan" "Ram"    

$Ford
[1] "Ranger"   "F150"     "Explorer"

基准测试

我很好奇这里的答案中哪一个是最快的,显然由@akrun使用的split明显是最快的,其次是unstack

enter image description here

bm <- microbenchmark::microbenchmark(
  akrun_split = with(my_data, split(list_values,
                                    factor(list_names, levels = unique(list_names)))),
  akrun_unstack = unstack(my_data, list_values ~ list_names),
  andrew_deframe1 = my_data |>
    group_by(list_names)  |>
    group_modify(\(x, ...) tibble(res = list(deframe(x)))) |>
    deframe(),
  andrew_deframe2 = my_data %>%
    group_by(list_names) %>%
    summarise(named_vec = list(list_values)) %>%
    deframe(),
  paulsmith = my_data %>% 
    group_by(list_names) %>% 
    summarise(list_values = list(list_values)) %>% 
    {set_names(.$list_values, .$list_names)}, 
  times=1000L
)

1
最后一个解决方案的一个好处是,很容易将第三列编码为值的名称。假设我们将lets列添加为第三列。然后,my_data$lets <- head(letters); my_data %>% summarise(named_vec = list(setNames(list_values, lets)), .by = list_names) %>% deframe - undefined

6
我们可以使用split函数。
with(my_data, split(list_values,
     factor(list_names, levels = unique(list_names))))
$Ford
[1] "Ranger"   "F150"     "Explorer"

$Chevy
[1] "Equinox"

$Dodge
[1] "Caravan" "Ram"   

或使用unstack

unstack(my_data, list_values ~ list_names)
$Chevy
[1] "Equinox"

$Dodge
[1] "Caravan" "Ram"    

$Ford
[1] "Ranger"   "F150"     "Explorer"

1
my_data %>% unstack(list_values ~ list_names - Jeff Parker
1
@JeffParker 如果您不想使用 factor 对值进行排序,那么 split 也是一种紧凑的选择。 - akrun
好观点。在我这种情况下,顺序并不重要。对他人来说是个很好的参考。 - Jeff Parker
看起来 splitunstack 快一点(只是出于好奇进行了基准测试)。 - AndrewGB
1
@AndrewGillreath-Brown split是一个快速函数,而stack/unstack则较慢。 - akrun

4

另一种可能的解决方案:

library(tidyverse)

my_data <- tibble(list_names = c("Ford", "Chevy", "Ford", "Dodge", "Dodge", "Ford"),
                  list_values = c("Ranger", "Equinox", "F150", "Caravan", "Ram", "Explorer"))

my_data %>% 
  group_by(list_names) %>% 
  summarise(list_values = list(list_values)) %>% 
  {set_names(.$list_values, .$list_names)}

#> $Chevy
#> [1] "Equinox"
#> 
#> $Dodge
#> [1] "Caravan" "Ram"    
#> 
#> $Ford
#> [1] "Ranger"   "F150"     "Explorer"

1

更新II: 感谢Jeff Parker的建议,将tibble的输出更改为向量输出。

更新: 根据Jeff Parker的评论(请参见评论),我现在更新了代码。问题在于将Names设置为未排序状态,在使用sort之后,我们可以正确地使用setNames。然后我添加了map来应用dplyrselect以删除每个数据帧中的第一列:

library(dplyr)
library(purrr)

my_data %>% 
  group_by(list_names) %>% 
  mutate(list_values= paste(list_values, collapse = ", ")) %>% 
  slice(1) %>% 
  group_split() %>% 
  setNames(sort(unique(my_data$list_names))) %>% 
  map(., dplyr::pull, -list_names) %>%
  map(., ~str_split(.x, ", ")[[1]] )

$Chevy
[1] "Equinox"

$Dodge
[1] "Caravan" "Ram"    

$Ford
[1] "Ranger"   "F150"     "Explorer"

1
列表元素应该是向量,而不是数据框。另外,你把雪佛兰和道奇搞混了,还把道奇和福特搞混了。 - Jeff Parker
@JeffParker。非常感谢您的留言。我会澄清这个问题。 - TarJae
@JeffParker,请查看我的更新! - TarJae
1
列表元素仍然是 tibbles,但请参考我的最新答案,使用 dplyr::pullmap(., str_split) 进行修复。 - Jeff Parker

1

只是为了好玩,我加入了使用for循环的基准测试和TarJae的答案。

bm <- microbenchmark::microbenchmark(
  akrun_split = with(my_data, split(list_values,
                                    factor(list_names, levels = unique(list_names)))),
  akrun_unstack = unstack(my_data, list_values ~ list_names),
  andrew_deframe1 = my_data |>
    group_by(list_names)  |>
    group_modify(\(x, ...) tibble(res = list(deframe(x)))) |>
    deframe(),
  andrew_deframe2 = my_data %>%
    group_by(list_names) %>%
    summarise(named_vec = list(list_values)) %>%
    deframe(),
  paulsmith = my_data %>% 
    group_by(list_names) %>% 
    summarise(list_values = list(list_values)) %>% 
    {set_names(.$list_values, .$list_names)},
  jeffs = {
    desired_output <- list()
    for(i in seq_along(my_data$list_names)) {
      entry <- my_data %>%
        filter(list_names == my_data$list_names[i]) %>%
        pull(list_values)
      desired_output[[my_data$list_names[i]]] <- entry
    }
    desired_output},
    TarJae = my_data %>% 
    group_by(list_names) %>% 
    mutate(list_values= paste(list_values, collapse = ", ")) %>% 
    slice(1) %>% 
    group_split() %>% 
    setNames(sort(unique(my_data$list_names))) %>% 
    map(., dplyr::pull, -list_names) %>%
    map(., ~str_split(.x, ", ")[[1]] ), 
  times=100L
)

enter image description here

我还在 Akrun 提供的两种最快选项上使用了更大的数据集进行基准测试。

library(nycflights13)
my_data <- nycflights13::flights %>%
  select(list_names = carrier, list_values = flight)

enter image description here


0

另一种方法:

library(tidyverse)

my_data %>%
  group_split(list_names) %>%
  map(~ lst(!!unique(pull(.x, list_names)) := unique(pull(.x, list_values)))) %>%
  flatten()
#> $Chevy
#> [1] "Equinox"
#> 
#> $Dodge
#> [1] "Caravan" "Ram"    
#> 
#> $Ford
#> [1] "Ranger"   "F150"     "Explorer"

2022-01-14使用reprex package(v2.0.1)创建

数据:

my_data <- tibble(
  list_names = c("Ford", "Chevy", "Ford", "Dodge", "Dodge", "Ford"),
  list_values = c("Ranger", "Equinox", "F150", "Caravan", "Ram", "Explorer")
)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接