#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 уровня CsetAttrib
. Пакетbit
, кстати, имеет ту же функцию. У него тоже естьvecseq
(я только что видел), что выглядит очень удобно. Возможно, стоит просмотретьbit
, чтобы увидеть, какие еще драгоценные камни у него есть (примечание для себя).