Самый эффективный метод преобразования списка в data.frame?

#performance #r #memory-management #dataframe #data.table

#Производительность #r #управление памятью #dataframe #data.table

Вопрос:

Только что поговорили об этом с коллегами, и мы подумали, что стоит посмотреть, что скажут люди из SO land. Предположим, у меня был список из N элементов, где каждый элемент был вектором длины X. Теперь предположим, что я хотел преобразовать это в data.frame. Как и в большинстве случаев в R, существует множество способов очистки от пресловутого cat, таких as.dataframe как использование пакета plyr, объединение do.call с cbind , предварительное выделение DF и его заполнение и другие.

Проблема, которая была представлена, заключалась в том, что происходит, когда либо N, либо X (в нашем случае это X) становится чрезвычайно большим. Существует ли какой-либо метод очистки cat, который заметно превосходит, когда важна эффективность (особенно с точки зрения памяти)?

Ответ №1:

Поскольку a data.frame уже является списком, и вы знаете, что каждый элемент списка имеет одинаковую длину (X), самым быстрым, вероятно, было бы просто обновить атрибуты class и row.names :

 set.seed(21)
n <- 1e6
x <- list(x=rnorm(n), y=rnorm(n), z=rnorm(n))
x <- c(x,x,x,x,x,x)

system.time(a <- as.data.frame(x))
system.time(b <- do.call(data.frame,x))
system.time({
  d <- x  # Skip 'c' so Joris doesn't down-vote me! ;-)
  class(d) <- "data.frame"
  rownames(d) <- 1:n
  names(d) <- make.unique(names(d))
})

identical(a, b)  # TRUE
identical(b, d)  # TRUE
  

Обновление — это в ~ 2 раза быстрее, чем создание d :

 system.time({
  e <- x
  attr(e, "row.names") <- c(NA_integer_,n)
  attr(e, "class") <- "data.frame"
  attr(e, "names") <- make.names(names(e), unique=TRUE)
})

identical(d, e)  # TRUE
  

Обновление 2 — Я забыл о потреблении памяти. Последнее обновление создает две копии e . Использование attributes функции сводит это только к одной копии.

 set.seed(21)
f <- list(x=rnorm(n), y=rnorm(n), z=rnorm(n))
f <- c(f,f,f,f,f,f)
tracemem(f)
system.time({  # makes 2 copies
  attr(f, "row.names") <- c(NA_integer_,n)
  attr(f, "class") <- "data.frame"
  attr(f, "names") <- make.names(names(f), unique=TRUE)
})

set.seed(21)
g <- list(x=rnorm(n), y=rnorm(n), z=rnorm(n))
g <- c(g,g,g,g,g,g)
tracemem(g)
system.time({  # only makes 1 copy
  attributes(g) <- list(row.names=c(NA_integer_,n),
    class="data.frame", names=make.names(names(g), unique=TRUE))
})

identical(f,g)  # TRUE
  

Комментарии:

1. Оставьте «вероятно» из ответа, и это правильно. Также будет правильно, если вы создадите функцию, используя эти вызовы и заменив обман в знании n командой длины. Ваша новая функция примерно эквивалентна data.frame() после удаления всех обширных проверок. Итак, если вы точно знаете, что передаете вызову правильные входные данные, тогда просто делайте то, что Джош рекомендовал для ускорения. Если вы не уверены, то data.frame безопаснее, а do.call(data.frame, x)) — следующий по быстродействию (как ни странно).

2. Смотрите plyr::quickdf именно эту функцию.

3. @John: Под «вероятно» я имел в виду «насколько мне известно». Я стараюсь не говорить слишком категорично, если я не совсем уверен.

4. Хорошая демонстрация tracemem в действии и хорошая иллюстрация разницы между списками и фреймами данных.

5. @hadley: канонический, по мнению кого? Я не могу найти никакого обсуждения этого в руководствах, и attr<- и structure , похоже, используются примерно одинаково часто в исходных текстах core R… и structure использует attributes<- .

Ответ №2:

Похоже, что для этого требуется data.table предложение, учитывая, что требуется эффективность для больших наборов данных. В частности, setattr устанавливается по ссылке и не копирует

 library(data.table)
set.seed(21)
n <- 1e6
h <- list(x=rnorm(n), y=rnorm(n), z=rnorm(n))
h <- c(h,h,h,h,h,h)
tracemem(h)

system.time({h <- as.data.table(h)
            setattr(h, 'names', make.names(names(h), unique=T))})
  

as.data.table , однако делает копию.


Редактировать — версия для копирования отсутствует

Используя предложение @MatthewDowle, setattr(h,'class','data.frame') которое преобразует в data.frame по ссылке (без копий)

 set.seed(21)
n <- 1e6
i <- list(x=rnorm(n), y=rnorm(n), z=rnorm(n))
i <- c(i,i,i,i,i,i)
tracemem(i)

system.time({  
  setattr(i, 'class', 'data.frame')
  setattr(i, "row.names", c(NA_integer_,n))

  setattr(i, "names", make.names(names(i), unique=TRUE))

})
  

Комментарии:

1. setattr(h, «class», «data.frame») должен быть мгновенным, вообще без копирования.

2. @MatthewDowle — Как есть setattr(h, "class", "data.table") 😉 (Кстати, очень круто).

3. @Josho’Brien Действительно 🙂 Только за последние несколько дней понял, что ?setattr говорит, что x должно быть data.table (благодаря комментарию к datatable-help). setattr на самом деле предназначен для работы с чем угодно. Исправит документ. Он также возвращает свои входные данные, так что вы можете [i,j,by] впоследствии, при необходимости, составить их (скажем, если вы обернете их в псевдоним: setDT(DF)[i,j,by] ).

4. @MatthewDowle — Да, я попробовал ваш код и был рад видеть, что он выполнил преобразование в data.frame без создания каких-либо копий. Отличный взлом!

5. @Josho’Brien setattr на самом деле всего лишь однострочная оболочка для функции R API уровня C setAttrib . Пакет bit , кстати, имеет ту же функцию. У него тоже есть vecseq (я только что видел), что выглядит очень удобно. Возможно, стоит просмотреть bit , чтобы увидеть, какие еще драгоценные камни у него есть (примечание для себя).