Подход Tidyverse к привязке безымянного списка безымянных векторов по строкам - эквивалент do.call (rbind, x) - PullRequest
29 голосов
/ 05 мая 2020

Я часто нахожу вопросы, когда люди каким-то образом заканчивают список безымянных векторов безымянных символьных векторов, и они хотят связать их построчно в data.frame. Вот пример:

library(magrittr)
data <- cbind(LETTERS[1:3],1:3,4:6,7:9,c(12,15,18)) %>%
  split(1:3) %>% unname
data
#[[1]]
#[1] "A"  "1"  "4"  "7"  "12"
#
#[[2]]
#[1] "B"  "2"  "5"  "8"  "15"
#
#[[3]]
#[1] "C"  "3"  "6"  "9"  "18"

Один из типичных подходов - использование do.call из базового R.

do.call(rbind, data) %>% as.data.frame
#  V1 V2 V3 V4 V5
#1  A  1  4  7 12
#2  B  2  5  8 15
#3  C  3  6  9 18

Возможно, менее эффективный подход - Reduce из базового R.

Reduce(rbind,data, init = NULL) %>% as.data.frame
#  V1 V2 V3 V4 V5
#1  A  1  4  7 12
#2  B  2  5  8 15
#3  C  3  6  9 18

Однако, когда мы рассматриваем более современные пакеты, такие как dplyr или data.table, некоторые из подходов, которые могут сразу прийти в голову, не работают, потому что векторы не имеют имени или не являются список.

library(dplyr)
bind_rows(data)
#Error: Argument 1 must have names
library(data.table)
rbindlist(data)
#Error in rbindlist(data) : 
#  Item 1 of input is not a data.frame, data.table or list

Один подход может заключаться в set_names на векторах.

library(purrr)
map_df(data, ~set_names(.x, seq_along(.x)))
# A tibble: 3 x 5
#  `1`   `2`   `3`   `4`   `5`  
#  <chr> <chr> <chr> <chr> <chr>
#1 A     1     4     7     12   
#2 B     2     5     8     15   
#3 C     3     6     9     18  

Однако кажется, что шагов больше, чем нужно.

Следовательно, мой вопрос заключается в том, какой эффективный подход tidyverse или data.table к привязке безымянного списка безымянных символьных векторов в строку data.frame -мудро?

Ответы [ 7 ]

14 голосов
/ 05 мая 2020

Не совсем уверен в эффективности, но компактный вариант с использованием purrr и tibble может быть:

map_dfc(purrr::transpose(data), ~ unlist(tibble(.)))

  V1    V2    V3    V4    V5   
  <chr> <chr> <chr> <chr> <chr>
1 A     1     4     7     12   
2 B     2     5     8     15   
3 C     3     6     9     18  
11 голосов
/ 05 мая 2020

Редактировать

Используйте подход @ sindri_baldur : { ссылка }


Способ с data.table, аналогично тому, что показал @tmfmnk

library(data.table)
as.data.table(transpose(data))
#   V1 V2 V3 V4 V5
#1:  A  1  4  7 12
#2:  B  2  5  8 15
#3:  C  3  6  9 18
10 голосов
/ 07 мая 2020
library(data.table)
setDF(transpose(data))

  V1 V2 V3 V4 V5
1  A  1  4  7 12
2  B  2  5  8 15
3  C  3  6  9 18
7 голосов
/ 07 мая 2020

Это кажется довольно компактным. Я считаю, что это то, что дает bind_rows() из dplyr и, следовательно, map_df() в purrr, поэтому должно быть достаточно эффективным.

library(vctrs)

vec_rbind(!!!data)

Это дает data.frame.

  ...1 ...2 ...3 ...4 ...5
1    A    1    4    7   12
2    B    2    5    8   15
3    C    3    6    9   18

Некоторые тесты

Похоже, что .name_repair в методах tidyverse является серьезным узким местом. Я выбрал несколько довольно простых вариантов, которые, похоже, быстрее всего работали из других сообщений (спасибо H 1 и sindri_baldur).

microbenchmark(vctrs = vec_rbind(!!!data),
               dt = rbindlist(lapply(data, as.list)),
               map = map_df(data, as_tibble_row, .name_repair = "unique"),
               base = as.data.frame(do.call(rbind, data)))

benchmark 1

Но если вы сначала назовете векторы (но не обязательно элементы списка), вы получите другую историю.

data2 <- modify(data, ~set_names(.x, seq(.x)))

microbenchmark(vctrs = vec_rbind(!!!data2),
               dt = rbindlist(lapply(data2, as.list)),
               map = map_df(data2, as_tibble_row),
               base = as.data.frame(do.call(rbind, data2)))

benchmark 2

Фактически, вы может включать в себя время для именования векторов в решении vec_rbind(), а не других, и при этом видеть довольно высокую производительность.

microbenchmark(vctrs = vec_rbind(!!!modify(data, ~set_names(.x, seq(.x)))),
               dt = setDF(transpose(data)),
               map = map_df(data2, as_tibble_row),
               base = as.data.frame(do.call(rbind, data)))

final benchmark

Для чего это стоит.

5 голосов
/ 06 мая 2020

Мой подход состоял бы в том, чтобы просто превратить эти записи списка в ожидаемый тип

rbindlist(lapply(data, as.list))
#       V1     V2     V3     V4     V5
#   <char> <char> <char> <char> <char>
#1:      A      1      4      7     12
#2:      B      2      5      8     15
#3:      C      3      6      9     18

Если вы хотите, чтобы ваши типы данных были скорректированы с вектора символов на соответствующие типы, тогда lapply может помочь и здесь. Первый lapply вызывается для каждой строки, второй lapply вызывается для каждого столбца.

rbindlist(lapply(data, as.list))[, lapply(.SD, type.convert)]
       V1    V2    V3    V4    V5
   <fctr> <int> <int> <int> <int>
1:      A     1     4     7    12
2:      B     2     5     8    15
3:      C     3     6     9    18
5 голосов
/ 05 мая 2020

Вариант с unnest_wider

library(tibble)
library(tidyr)
library(stringr)
tibble(col = data) %>%
    unnest_wider(c(col), names_repair = ~ str_c('value', seq_along(.)))
# A tibble: 3 x 5
#  value1 value2 value3 value4 value5
#  <chr>  <chr>  <chr>  <chr>  <chr> 
#1 A      1      4      7      12    
#2 B      2      5      8      15    
#3 C      3      6      9      18    
3 голосов
/ 06 мая 2020

Вот небольшой вариант предлагаемого tmfmnk подхода с использованием as_tibble_row() для преобразования векторов в однострочные таблицы. Также необходимо использовать аргумент .name_repair:

library(purrr)
library(tibble)

map_df(data, as_tibble_row, .name_repair = ~paste0("value", seq(.x)))

# A tibble: 3 x 5
  value1 value2 value3 value4 value5
  <chr>  <chr>  <chr>  <chr>  <chr> 
1 A      1      4      7      12    
2 B      2      5      8      15    
3 C      3      6      9      18
...