Выращивание кадра данных эффективным образом
Ответ 1
Первый подход
Я попытался получить доступ к каждому элементу предварительно выделенного файла data.frame:
res <- data.frame(x=rep(NA,1000), y=rep(NA,1000))
tracemem(res)
for(i in 1:1000) {
res[i,"x"] <- runif(1)
res[i,"y"] <- rnorm(1)
}
Но tracemem сходит с ума (например, каждый раз копируется data.frame на новый адрес).
Альтернативный подход (тоже не работает)
Один подход (не уверен, что он быстрее, чем я еще не тестировал) состоит в том, чтобы создать список data.frames, а затем stack
все вместе:
makeRow <- function() data.frame(x=runif(1),y=rnorm(1))
res <- replicate(1000, makeRow(), simplify=FALSE ) # returns a list of data.frames
library(taRifx)
res.df <- stack(res)
К сожалению, при создании списка я думаю, что вам будет сложно предварительно выделить. Например:
> tracemem(res)
[1] "<0x79b98b0>"
> res[[2]] <- data.frame()
tracemem[0x79b98b0 -> 0x71da500]:
Другими словами, замена элемента списка приводит к копированию списка. Я принимаю весь список, но это возможно только для этого элемента списка. Я не очень хорошо разбираюсь в деталях управления памятью R.
Вероятно, лучший подход
Как и во многих процессах с ограниченной скоростью или ограничением памяти, в настоящее время наилучшим подходом может быть использование data.table
вместо data.frame
. Поскольку data.table
имеет назначение :=
по ссылочному оператору, он может обновляться без повторного копирования:
library(data.table)
dt <- data.table(x=rep(0,1000), y=rep(0,1000))
tracemem(dt)
for(i in 1:1000) {
dt[i,x := runif(1)]
dt[i,y := rnorm(1)]
}
# note no message from tracemem
Но, как указывает @MatthewDowle, set()
- это подходящий способ сделать это внутри цикла. Это делает его еще быстрее:
library(data.table)
n <- 10^6
dt <- data.table(x=rep(0,n), y=rep(0,n))
dt.colon <- function(dt) {
for(i in 1:n) {
dt[i,x := runif(1)]
dt[i,y := rnorm(1)]
}
}
dt.set <- function(dt) {
for(i in 1:n) {
set(dt,i,1L, runif(1) )
set(dt,i,2L, rnorm(1) )
}
}
library(microbenchmark)
m <- microbenchmark(dt.colon(dt), dt.set(dt),times=2)
(Результаты показаны ниже)
Бенчмаркинг
Когда цикл работает 10000 раз, таблица данных почти на порядок выше:
Unit: seconds
expr min lq median uq max
1 test.df() 523.49057 523.49057 524.52408 525.55759 525.55759
2 test.dt() 62.06398 62.06398 62.98622 63.90845 63.90845
3 test.stack() 1196.30135 1196.30135 1258.79879 1321.29622 1321.29622
И сравнение :=
с set()
:
> m
Unit: milliseconds
expr min lq median uq max
1 dt.colon(dt) 654.54996 654.54996 656.43429 658.3186 658.3186
2 dt.set(dt) 13.29612 13.29612 15.02891 16.7617 16.7617
Заметим, что n
здесь 10 ^ 6 не 10 ^ 5, как в приведенных выше тестах. Таким образом, порядок на порядок больше, и результат измеряется в миллисекундах, а не секундах. Впечатляет действительно.
Ответ 2
Мне нравится RSQLite
: dbWriteTable(...,append=TRUE)
при сборке и dbReadTable
в конце.
Если данные достаточно малы, можно использовать файл ": memory:", если он большой, жесткий диск.
Конечно, он не может конкурировать с точки зрения скорости:
makeRow <- function() data.frame(x=runif(1),y=rnorm(1))
library(RSQLite)
con <- dbConnect(RSQLite::SQLite(), ":memory:")
collect1 <- function(n) {
for (i in 1:n) dbWriteTable(con, "test", makeRow(), append=TRUE)
dbReadTable(con, "test", row.names=NULL)
}
collect2 <- function(n) {
res <- data.frame(x=rep(NA, n), y=rep(NA, n))
for(i in 1:n) res[i,] <- makeRow()[1,]
res
}
> system.time(collect1(1000))
User System verstrichen
7.01 0.00 7.05
> system.time(collect2(1000))
User System verstrichen
0.80 0.01 0.81
Но это может выглядеть лучше, если data.frame
имеет более одной строки. И вам не нужно знать количество строк заранее.
Ответ 3
У вас также может быть пустой объект списка, где элементы заполняются с помощью dataframes; затем собирайте результаты в конце с помощью sapply или аналогичного. Ниже приведен пример здесь. Это не приведет к штрафам за увеличение объекта.
Ответ 4
Ну, я очень удивлен, что никто не упоминал о преобразовании в матрицу...
Сравнение с функциями dt.colon и dt.set, определенными Ari B Friedman, преобразование в матрицу имеет лучшее время работы (немного быстрее, чем dt.colon). Все аффекты внутри матрицы выполняются с помощью ссылки, поэтому в этом коде нет лишней копии памяти.
CODE:
library(data.table)
n <- 10^4
dt <- data.table(x=rep(0,n), y=rep(0,n))
use.matrix <- function(dt) {
mat = as.matrix(dt) # converting to matrix
for(i in 1:n) {
mat[i,1] = runif(1)
mat[i,2] = rnorm(1)
}
return(as.data.frame(mat)) # converting back to a data.frame
}
dt.colon <- function(dt) { # same as Ari function
for(i in 1:n) {
dt[i,x := runif(1)]
dt[i,y := rnorm(1)]
}
}
dt.set <- function(dt) { # same as Ari function
for(i in 1:n) {
set(dt,i,1L, runif(1) )
set(dt,i,2L, rnorm(1) )
}
}
library(microbenchmark)
microbenchmark(dt.colon(dt), dt.set(dt), use.matrix(dt),times=10)
РЕЗУЛЬТАТ:
Unit: milliseconds
expr min lq median uq max neval
dt.colon(dt) 7107.68494 7193.54792 7262.76720 7277.24841 7472.41726 10
dt.set(dt) 93.25954 94.10291 95.07181 97.09725 99.18583 10
use.matrix(dt) 48.15595 51.71100 52.39375 54.59252 55.04192 10
Плюсы использования матрицы:
- это самый быстрый метод до сих пор
- Вам не нужно изучать/использовать объекты data.table
Con с использованием матрицы:
- вы можете обрабатывать только один тип данных в матрице (в частности, если у вас были смешанные типы в столбцах вашего data.frame, то все они будут преобразованы в символ по строке: mat = as. матрица (dt) # преобразование в матрицу)