Преобразовать список фреймов данных в один фрейм данных

336

У меня есть код, который в одном месте заканчивается списком фреймов данных, которые я действительно хочу преобразовать в один большой фрейм данных.

Я получил несколько советов из предыдущего вопроса, который пытался сделать что-то похожее, но более сложное.

Вот пример того, с чего я начинаю (это сильно упрощено для иллюстрации):

listOfDataFrames <- vector(mode = "list", length = 100)

for (i in 1:100) {
    listOfDataFrames[[i]] <- data.frame(a=sample(letters, 500, rep=T),
                             b=rnorm(500), c=rnorm(500))
}

Я в настоящее время использую это:

  df <- do.call("rbind", listOfDataFrames)
JD Long
источник
Также смотрите этот вопрос: stackoverflow.com/questions/2209258/…
Шейн
27
do.call("rbind", list)Идиома , что я использовал до того, как хорошо. Зачем вам нужно начальное unlist?
Дирк Эддельбюттель
5
Может ли кто-нибудь объяснить мне разницу между do.call ("rbind", list) и rbind (list) - почему результаты не совпадают?
user6571411
1
@ user6571411 Поскольку do.call () не возвращает аргументы один за другим, а использует список для хранения аргументов функции. См. Https://www.stat.berkeley.edu/~s133/Docall.html
Маржолейн Фоккема,

Ответы:

131

Используйте bind_rows () из пакета dplyr:

bind_rows(list_of_dataframes, .id = "column_label")
joeklieg
источник
5
Хорошее решение. .id = "column_label"добавляет уникальные имена строк на основе имен элементов списка.
Сибо Цзян
10
так как это 2018 год, и dplyrон быстрый и надежный инструмент, я изменил его на принятый ответ. Годы летят!
JD Long
186

Еще один вариант - использовать функцию plyr:

df <- ldply(listOfDataFrames, data.frame)

Это немного медленнее, чем оригинал:

> system.time({ df <- do.call("rbind", listOfDataFrames) })
   user  system elapsed 
   0.25    0.00    0.25 
> system.time({ df2 <- ldply(listOfDataFrames, data.frame) })
   user  system elapsed 
   0.30    0.00    0.29
> identical(df, df2)
[1] TRUE

Я предполагаю, что использование do.call("rbind", ...)будет самым быстрым подходом, который вы найдете, если только вы не можете сделать что-то вроде: (а) использовать матрицы вместо data.frames и (б) предварительно выделить конечную матрицу и присвоить ей, а не увеличивать ее ,

Изменить 1 :

Основываясь на комментарии Хэдли, вот последняя версия rbind.fillCRAN:

> system.time({ df3 <- rbind.fill(listOfDataFrames) })
   user  system elapsed 
   0.24    0.00    0.23 
> identical(df, df3)
[1] TRUE

Это проще, чем rbind, и незначительно быстрее (эти временные интервалы сохраняются в течение нескольких прогонов). И, насколько я понимаю, версия plyrна github еще быстрее, чем эта.

Шейн
источник
28
rbind.fill в последней версии plyr значительно быстрее, чем do.call и rbind
hadley
1
интересный. для меня rbind.fill был самым быстрым. Как ни странно, do.call / rbind не вернул идентичное ИСТИНА, даже если я не смог найти разницу. Два других были равны, но Плир был медленнее.
Мэтт Баннер
I()может заменить data.frameв вашем ldplyпризыве
крестить
4
есть также melt.listв форме (2)
крестить
do.call(function(...) rbind(..., make.row.names=F), df)полезно, если вы не хотите автоматически генерируемые уникальные имена строк.
августа
111

В целях полноты я думал, что ответы на этот вопрос требуют обновления. «Я предполагаю, что использование do.call("rbind", ...)будет самым быстрым подходом, который вы найдете ...» Это, вероятно, было верно для мая 2010 года и некоторое время спустя, но примерно в сентябре 2011 года rbindlistв data.tableпакете версии 1.8.2 была введена новая функция. , с замечанием, что «Это так же, как do.call("rbind",l), но гораздо быстрее». Насколько быстрее?

library(rbenchmark)
benchmark(
  do.call = do.call("rbind", listOfDataFrames),
  plyr_rbind.fill = plyr::rbind.fill(listOfDataFrames), 
  plyr_ldply = plyr::ldply(listOfDataFrames, data.frame),
  data.table_rbindlist = as.data.frame(data.table::rbindlist(listOfDataFrames)),
  replications = 100, order = "relative", 
  columns=c('test','replications', 'elapsed','relative')
  ) 

                  test replications elapsed relative
4 data.table_rbindlist          100    0.11    1.000
1              do.call          100    9.39   85.364
2      plyr_rbind.fill          100   12.08  109.818
3           plyr_ldply          100   15.14  137.636
andrekos
источник
3
Огромное спасибо за это - я выдернул свои волосы, потому что мои наборы данных становились слишком большими для ldplyсвязки длинных расплавленных кадров данных. В любом случае, я получил невероятное ускорение благодаря вашему rbindlistпредложению.
KarateSnowMachine
11
И еще один для полноты: также dplyr::rbind_all(listOfDataFrames)добьется цели.
andyteucher
2
есть ли эквивалент, rbindlistно который добавляет кадры данных по столбцам? что-то вроде cbindlist?
rafa.pereira
2
@ rafa.pereira Недавно поступил запрос на добавление функции
Henrik
Я также вырывал свои волосы, потому do.call()что 18 часов работал со списком фреймов данных, и все еще не закончил, спасибо !!!
Грэм Фрост
74

связывать-участок

Код:

library(microbenchmark)

dflist <- vector(length=10,mode="list")
for(i in 1:100)
{
  dflist[[i]] <- data.frame(a=runif(n=260),b=runif(n=260),
                            c=rep(LETTERS,10),d=rep(LETTERS,10))
}


mb <- microbenchmark(
plyr::rbind.fill(dflist),
dplyr::bind_rows(dflist),
data.table::rbindlist(dflist),
plyr::ldply(dflist,data.frame),
do.call("rbind",dflist),
times=1000)

ggplot2::autoplot(mb)

сессия:

R version 3.3.0 (2016-05-03)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 7 x64 (build 7601) Service Pack 1

> packageVersion("plyr")
[1]1.8.4> packageVersion("dplyr")
[1]0.5.0> packageVersion("data.table")
[1]1.9.6

ОБНОВЛЕНИЕ : Rerun 31-Jan-2018. Бежал на одном компьютере. Новые версии пакетов. Добавлено семя для любителей семян.

введите описание изображения здесь

set.seed(21)
library(microbenchmark)

dflist <- vector(length=10,mode="list")
for(i in 1:100)
{
  dflist[[i]] <- data.frame(a=runif(n=260),b=runif(n=260),
                            c=rep(LETTERS,10),d=rep(LETTERS,10))
}


mb <- microbenchmark(
  plyr::rbind.fill(dflist),
  dplyr::bind_rows(dflist),
  data.table::rbindlist(dflist),
  plyr::ldply(dflist,data.frame),
  do.call("rbind",dflist),
  times=1000)

ggplot2::autoplot(mb)+theme_bw()


R version 3.4.0 (2017-04-21)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 7 x64 (build 7601) Service Pack 1

> packageVersion("plyr")
[1]1.8.4> packageVersion("dplyr")
[1]0.7.2> packageVersion("data.table")
[1]1.10.4

ОБНОВЛЕНИЕ : Rerun 06-Aug-2019.

введите описание изображения здесь

set.seed(21)
library(microbenchmark)

dflist <- vector(length=10,mode="list")
for(i in 1:100)
{
  dflist[[i]] <- data.frame(a=runif(n=260),b=runif(n=260),
                            c=rep(LETTERS,10),d=rep(LETTERS,10))
}


mb <- microbenchmark(
  plyr::rbind.fill(dflist),
  dplyr::bind_rows(dflist),
  data.table::rbindlist(dflist),
  plyr::ldply(dflist,data.frame),
  do.call("rbind",dflist),
  purrr::map_df(dflist,dplyr::bind_rows),
  times=1000)

ggplot2::autoplot(mb)+theme_bw()

R version 3.6.0 (2019-04-26)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 18.04.2 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/openblas/libblas.so.3
LAPACK: /usr/lib/x86_64-linux-gnu/libopenblasp-r0.2.20.so

packageVersion("plyr")
packageVersion("dplyr")
packageVersion("data.table")
packageVersion("purrr")

>> packageVersion("plyr")
[1]1.8.4>> packageVersion("dplyr")
[1]0.8.3>> packageVersion("data.table")
[1]1.12.2>> packageVersion("purrr")
[1]0.3.2
RMF
источник
2
Это отличный ответ. Я запускал одно и то же (та же ОС, те же пакеты, разные рандомизации, потому что вы этого не делаете set.seed), но видел некоторые различия в производительности в худшем случае. rbindlistна самом деле у меня был лучший наихудший случай, а также лучший типичный случай в моих результатах
C8H10N4O2
48

Существует также bind_rows(x, ...)в dplyr.

> system.time({ df.Base <- do.call("rbind", listOfDataFrames) })
   user  system elapsed 
   0.08    0.00    0.07 
> 
> system.time({ df.dplyr <- as.data.frame(bind_rows(listOfDataFrames)) })
   user  system elapsed 
   0.01    0.00    0.02 
> 
> identical(df.Base, df.dplyr)
[1] TRUE
TheVTM
источник
технически говоря, вам не нужен as.data.frame - все, что делает его, делает его исключительно data.frame, в отличие от table_df (from deplyr)
user1617979
14

Вот еще один способ сделать это (просто добавив его в ответы, потому что reduce это очень эффективный функциональный инструмент, который часто упускают из виду как замену циклов. В данном конкретном случае, ни один из них не является значительно более быстрым, чем do.call)

используя базу R:

df <- Reduce(rbind, listOfDataFrames)

или, используя Tidyverse:

library(tidyverse) # or, library(dplyr); library(purrr)
df <- listOfDataFrames %>% reduce(bind_rows)
yeedle
источник
11

Как это сделать в тидиверсе:

df.dplyr.purrr <- listOfDataFrames %>% map_df(bind_rows)
Ник
источник
3
Зачем вам использовать, mapесли bind_rowsможно взять список данных?
смотри
9

Обновленный визуальный материал для тех, кто хочет сравнить некоторые из недавних ответов (я хотел сравнить решение purrr и dplyr). В основном я объединил ответы от @TheVTM и @rmf.

введите описание изображения здесь

Код:

library(microbenchmark)
library(data.table)
library(tidyverse)

dflist <- vector(length=10,mode="list")
for(i in 1:100)
{
  dflist[[i]] <- data.frame(a=runif(n=260),b=runif(n=260),
                            c=rep(LETTERS,10),d=rep(LETTERS,10))
}


mb <- microbenchmark(
  dplyr::bind_rows(dflist),
  data.table::rbindlist(dflist),
  purrr::map_df(dflist, bind_rows),
  do.call("rbind",dflist),
  times=500)

ggplot2::autoplot(mb)

Информация о сессии:

sessionInfo()
R version 3.4.1 (2017-06-30)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 7 x64 (build 7601) Service Pack 1

Версии пакета:

> packageVersion("tidyverse")
[1]1.1.1> packageVersion("data.table")
[1]1.10.0
Новая звезда
источник
7

Единственное что решения с data.table не хватает - это столбец идентификатора, чтобы узнать, с какого кадра данных в списке поступают данные.

Что-то вроде этого:

df_id <- data.table::rbindlist(listOfDataFrames, idcol = TRUE)

idcolПараметр добавляет столбец ( .id) , идентифицирующий происхождение dataframe , содержащейся в списке. Результат будет выглядеть примерно так:

.id a         b           c
1   u   -0.05315128 -1.31975849 
1   b   -1.00404849 1.15257952  
1   y   1.17478229  -0.91043925 
1   q   -1.65488899 0.05846295  
1   c   -1.43730524 0.95245909  
1   b   0.56434313  0.93813197  
f0nzie
источник