Каков наиболее эффективный способ создать список как фрейм данных?
Очень часто я хочу преобразовать список, в котором каждый индекс имеет идентичные типы элементов, во фрейм данных. Например, у меня может быть список:
> my.list
[[1]]
[[1]]$global_stdev_ppb
[1] 24267673
[[1]]$range
[1] 0.03114799
[[1]]$tok
[1] "hello"
[[1]]$global_freq_ppb
[1] 211592.6
[[2]]
[[2]]$global_stdev_ppb
[1] 11561448
[[2]]$range
[1] 0.08870838
[[2]]$tok
[1] "world"
[[2]]$global_freq_ppb
[1] 1002043
Я хочу преобразовать этот список во фрейм данных, где каждый элемент индекса является столбцом. Естественная (для меня) вещь, чтобы пойти, это использовать do.call
:
> my.matrix<-do.call("rbind", my.list)
> my.matrix
global_stdev_ppb range tok global_freq_ppb
[1,] 24267673 0.03114799 "hello" 211592.6
[2,] 11561448 0.08870838 "world" 1002043
Достаточно просто, но когда я пытаюсь преобразовать эту матрицу как фрейм данных, столбцы остаются элементами списка, а не векторами:
> my.df<-as.data.frame(my.matrix, stringsAsFactors=FALSE)
> my.df[,1]
[[1]]
[1] 24267673
[[2]]
[1] 11561448
В настоящее время для правильного приведения кадра данных я перебираю каждый столбец, используя unlist
а также as.vector
, затем преобразовать фрейм данных как таковой:
new.list<-lapply(1:ncol(my.matrix), function(x) as.vector(unlist(my.matrix[,x])))
my.df<-as.data.frame(do.call(cbind, new.list), stringsAsFactors=FALSE)
Это, однако, кажется очень неэффективным. Есть ли лучший способ сделать это?
7 ответов
Я думаю, что вы хотите:
> do.call(rbind, lapply(my.list, data.frame, stringsAsFactors=FALSE))
global_stdev_ppb range tok global_freq_ppb
1 24267673 0.03114799 hello 211592.6
2 11561448 0.08870838 world 1002043.0
> str(do.call(rbind, lapply(my.list, data.frame, stringsAsFactors=FALSE)))
'data.frame': 2 obs. of 4 variables:
$ global_stdev_ppb: num 24267673 11561448
$ range : num 0.0311 0.0887
$ tok : chr "hello" "world"
$ global_freq_ppb : num 211593 1002043
Другой вариант:
data.frame(t(sapply(mylist, `[`)))
но эта простая манипуляция приводит к фрейму данных списков:
> str(data.frame(t(sapply(mylist, `[`))))
'data.frame': 2 obs. of 3 variables:
$ a:List of 2
..$ : num 1
..$ : num 2
$ b:List of 2
..$ : num 2
..$ : num 3
$ c:List of 2
..$ : chr "a"
..$ : chr "b"
Альтернативой этому, по той же схеме, но теперь такой же результат, как и у других решений, является:
data.frame(lapply(data.frame(t(sapply(mylist, `[`))), unlist))
[Правка: включены временные значения двух решений @Martin Morgan, которые имеют преимущество перед другим решением, возвращающим фрейм данных векторов.] Некоторые репрезентативные временные характеристики очень простой задачи:
mylist <- list(list(a = 1, b = 2, c = "a"), list(a = 2, b = 3, c = "b"))
> ## @Joshua Ulrich's solution:
> system.time(replicate(1000, do.call(rbind, lapply(mylist, data.frame,
+ stringsAsFactors=FALSE))))
user system elapsed
1.740 0.001 1.750
> ## @JD Long's solution:
> system.time(replicate(1000, do.call(rbind, lapply(mylist, data.frame))))
user system elapsed
2.308 0.002 2.339
> ## my sapply solution No.1:
> system.time(replicate(1000, data.frame(t(sapply(mylist, `[`)))))
user system elapsed
0.296 0.000 0.301
> ## my sapply solution No.2:
> system.time(replicate(1000, data.frame(lapply(data.frame(t(sapply(mylist, `[`))),
+ unlist))))
user system elapsed
1.067 0.001 1.091
> ## @Martin Morgan's Map() sapply() solution:
> f = function(x) function(i) sapply(x, `[[`, i)
> system.time(replicate(1000, as.data.frame(Map(f(mylist), names(mylist[[1]])))))
user system elapsed
0.775 0.000 0.778
> ## @Martin Morgan's Map() lapply() unlist() solution:
> f = function(x) function(i) unlist(lapply(x, `[[`, i), use.names=FALSE)
> system.time(replicate(1000, as.data.frame(Map(f(mylist), names(mylist[[1]])))))
user system elapsed
0.653 0.000 0.658
Я не могу сказать вам, что это "самый эффективный" с точки зрения памяти или скорости, но он довольно эффективен с точки зрения кодирования:
my.df <- do.call("rbind", lapply(my.list, data.frame))
шаг lapply() с data.frame() превращает каждый элемент списка в единый фрейм данных, который затем хорошо работает с rbind()
Хотя на этот вопрос уже давно дан ответ, стоит отметить data.table
пакет имеет rbindlist
которая выполняет эту задачу очень быстро:
library(microbenchmark)
library(data.table)
l <- replicate(1E4, list(a=runif(1), b=runif(1), c=runif(1)), simplify=FALSE)
microbenchmark( times=5,
R=as.data.frame(Map(f(l), names(l[[1]]))),
dt=data.frame(rbindlist(l))
)
дает мне
Unit: milliseconds
expr min lq median uq max neval
R 31.060119 31.403943 32.278537 32.370004 33.932700 5
dt 2.271059 2.273157 2.600976 2.635001 2.729421 5
Это
f = function(x) function(i) sapply(x, `[[`, i)
это функция, которая возвращает функцию, которая извлекает i-й элемент из x. Так
Map(f(mylist), names(mylist[[1]]))
получает именованный (спасибо Map!) список векторов, которые можно преобразовать во фрейм данных
as.data.frame(Map(f(mylist), names(mylist[[1]])))
Для скорости это обычно быстрее использовать unlist(lapply(...), use.names=FALSE)
как
f = function(x) function(i) unlist(lapply(x, `[[`, i), use.names=FALSE)
Более общий вариант
f = function(X, FUN) function(...) sapply(X, FUN, ...)
Когда появляются структуры списков? Может быть, есть более ранний шаг, когда итерация может быть заменена чем-то более векторизованным?
Пакет dplyr bind_rows
является эффективным.
one <- mtcars[1:4, ]
two <- mtcars[11:14, ]
system.time(dplyr::bind_rows(one, two))
user system elapsed
0.001 0.000 0.001
Не уверен, где они ранжируются, насколько эффективность, но в зависимости от структуры ваших списков, есть некоторые tidyverse
опции. Бонус в том, что они прекрасно работают с списками разной длины:
l <- list(a = list(var.1 = 1, var.2 = 2, var.3 = 3)
, b = list(var.1 = 4, var.2 = 5)
, c = list(var.1 = 7, var.3 = 9)
, d = list(var.1 = 10, var.2 = 11, var.3 = NA))
df <- dplyr::bind_rows(l)
df <- purrr::map_df(l, dplyr::bind_rows)
df <- purrr::map_df(l, ~.x)
# all create the same data frame:
# A tibble: 4 x 3
var.1 var.2 var.3
<dbl> <dbl> <dbl>
1 1 2 3
2 4 5 NA
3 7 NA 9
4 10 11 NA
И вы также можете смешивать векторы и фреймы данных:
library(dplyr)
bind_rows(
list(a = 1, b = 2),
data_frame(a = 3:4, b = 5:6),
c(a = 7)
)
# A tibble: 4 x 2
a b
<dbl> <dbl>
1 1 2
2 3 5
3 4 6
4 7 NA