Разгледах StackOverflow, но не мога да намеря решение, специфично за моя проблем, който включва добавяне на редове към рамка от данни в R.
Инициирам празна рамка от данни с 2 колони, както следва.
df = data.frame(x = numeric(), y = character())
След това целта ми е да итерирам през списък със стойности и при всяка итерация да добавям стойност в края на списъка. Започнах със следния код.
for (i in 1:10) {
df$x = rbind(df$x, i)
df$y = rbind(df$y, toString(i))
}
Опитах се да използвам и функциите c
, append
и merge
, но без успех. Моля, уведомете ме, ако имате някакви предложения.
Без да знам какво се опитвате да направите, ще споделя още едно предложение: Предварително разпределете вектори от типа, който искате, за всяка колона, вмъкнете стойности в тези вектори и след това, накрая, създайте вашия data.frame
.
Продължавайки с Julian's f3
(предварително разпределен data.frame
) като най-бързия вариант досега, дефиниран като:
# pre-allocate space
f3 <- function(n){
df <- data.frame(x = numeric(n), y = character(n), stringsAsFactors = FALSE)
for(i in 1:n){
df$x[i] <- i
df$y[i] <- toString(i)
}
df
}
Ето и подобен подход, но при който data.frame
се създава като последна стъпка.
# Use preallocated vectors
f4 <- function(n) {
x <- numeric(n)
y <- character(n)
for (i in 1:n) {
x[i] <- i
y[i] <- i
}
data.frame(x, y, stringsAsFactors=FALSE)
}
microbenchmark
от пакета "microbenchmark" ще ни даде по-обстойна представа, отколкото system.time
:
library(microbenchmark)
microbenchmark(f1(1000), f3(1000), f4(1000), times = 5)
# Unit: milliseconds
# expr min lq median uq max neval
# f1(1000) 1024.539618 1029.693877 1045.972666 1055.25931 1112.769176 5
# f3(1000) 149.417636 150.529011 150.827393 151.02230 160.637845 5
# f4(1000) 7.872647 7.892395 7.901151 7.95077 8.049581 5
f1()
(подходът по-долу) е изключително неефективен поради това, че често се извиква data.frame
, и поради това, че увеличаването на обектите по този начин е бавно в R. f3()
е много подобрен поради предварителното разпределение, но самата структура data.frame
може да е част от тясното място тук. f4()
се опитва да заобиколи това тясно място, без да компрометира подхода, който искате да приложите.
Това наистина не е добра идея, но ако искате да го направите по този начин, предполагам, че можете да опитате:
for (i in 1:10) {
df <- rbind(df, data.frame(x = i, y = toString(i)))
}
Обърнете внимание, че във вашия код има още един проблем:
stringsAsFactors
, ако искате символите да не се превръщат във фактори. Използвайте: df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
Нека направим сравнителен анализ на трите предложени решения:
# use rbind
f1 <- function(n){
df <- data.frame(x = numeric(), y = character())
for(i in 1:n){
df <- rbind(df, data.frame(x = i, y = toString(i)))
}
df
}
# use list
f2 <- function(n){
df <- data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
for(i in 1:n){
df[i,] <- list(i, toString(i))
}
df
}
# pre-allocate space
f3 <- function(n){
df <- data.frame(x = numeric(1000), y = character(1000), stringsAsFactors = FALSE)
for(i in 1:n){
df$x[i] <- i
df$y[i] <- toString(i)
}
df
}
system.time(f1(1000))
# user system elapsed
# 1.33 0.00 1.32
system.time(f2(1000))
# user system elapsed
# 0.19 0.00 0.19
system.time(f3(1000))
# user system elapsed
# 0.14 0.00 0.14
Най-доброто решение е да се разпредели предварително пространството (както е предвидено в R). Следващото най-добро решение е да се използва list
, а най-лошото решение (поне на базата на тези резултати от измерването на времето) изглежда е rbind
.
Да предположим, че просто не знаете предварително размера на data.frame. Той може да бъде както няколко реда, така и няколко милиона. Трябва да имате някакъв контейнер, който да се увеличава динамично. Като взех предвид моя опит и всички свързани с него отговори в SO, стигнах до 4 различни решения:
rbindlist
към data.frame
Използвайте бързата операция data.table
'и я съчетайте с ръчно удвояване на таблицата, когато е необходимо.
Използвайте RSQLite
и добавете към таблицата, която се съхранява в паметта.
data.frame
'собствена способност за нарастване и използване на потребителска среда (която има референтна семантика) за съхраняване на data.frame, така че да не бъде копирана при връщане.
Ето тест на всички методи както за малък, така и за голям брой добавени редове. Всеки метод има 3 функции, свързани с него:
create(first_element)
, която връща подходящия обект за подложка с поставен first_element
.
append(object, element)
, която добавя element
към края на таблицата (представена от object
).
access(object)
получава data.frame
с всички вмъкнати елементи.
rbindlist
към data.frameТова е доста лесно и просто:
create.1<-function(elems)
{
return(as.data.table(elems))
}
append.1<-function(dt, elems)
{
return(rbindlist(list(dt, elems),use.names = TRUE))
}
access.1<-function(dt)
{
return(dt)
}
data.table::set
+ ръчно удвояване на таблицата, когато е необходимо.Ще съхранявам истинската дължина на таблицата в атрибута rowcount
.
create.2<-function(elems)
{
return(as.data.table(elems))
}
append.2<-function(dt, elems)
{
n<-attr(dt, 'rowcount')
if (is.null(n))
n<-nrow(dt)
if (n==nrow(dt))
{
tmp<-elems[1]
tmp[[1]]<-rep(NA,n)
dt<-rbindlist(list(dt, tmp), fill=TRUE, use.names=TRUE)
setattr(dt,'rowcount', n)
}
pos<-as.integer(match(names(elems), colnames(dt)))
for (j in seq_along(pos))
{
set(dt, i=as.integer(n+1), pos[[j]], elems[[j]])
}
setattr(dt,'rowcount',n+1)
return(dt)
}
access.2<-function(elems)
{
n<-attr(elems, 'rowcount')
return(as.data.table(elems[1:n,]))
}
RSQLite
.Това в общи линии е copy&paste на отговора на Karsten W. в подобна тема.
create.3<-function(elems)
{
con <- RSQLite::dbConnect(RSQLite::SQLite(), ":memory:")
RSQLite::dbWriteTable(con, 't', as.data.frame(elems))
return(con)
}
append.3<-function(con, elems)
{
RSQLite::dbWriteTable(con, 't', as.data.frame(elems), append=TRUE)
return(con)
}
access.3<-function(con)
{
return(RSQLite::dbReadTable(con, "t", row.names=NULL))
}
data.frame
'собствена среда за добавяне на редове + потребителска среда.create.4<-function(elems)
{
env<-new.env()
env$dt<-as.data.frame(elems)
return(env)
}
append.4<-function(env, elems)
{
env$dt[nrow(env$dt)+1,]<-elems
return(env)
}
access.4<-function(env)
{
return(env$dt)
}
За удобство ще използвам една тестова функция, за да обхвана всички тях с непряко извикване. (Проверих: използването на do.call
вместо директното извикване на функциите не води до измеримо по-дълго изпълнение на кода).
test<-function(id, n=1000)
{
n<-n-1
el<-list(a=1,b=2,c=3,d=4)
o<-do.call(paste0('create.',id),list(el))
s<-paste0('append.',id)
for (i in 1:n)
{
o<-do.call(s,list(o,el))
}
return(do.call(paste0('access.', id), list(o)))
}
Нека да видим производителността за n=10 вмъквания.
Добавих и 'плацебо' функции (със суфикс 0
), които не изпълняват нищо - просто за да измеря натоварването на тестовата конфигурация.
r<-microbenchmark(test(0,n=10), test(1,n=10),test(2,n=10),test(3,n=10), test(4,n=10))
autoplot(r)
За 1E5 реда (измерванията са направени на процесор Intel(R) Core(TM) i7-4710HQ @ 2,50GHz):
nr function time
4 data.frame 228.251
3 sqlite 133.716
2 data.table 3.059
1 rbindlist 169.998
0 placebo 0.202
Изглежда, че базираната на SQLite сулюция, въпреки че възвръща известна скорост при големи данни, не се доближава до експоненциалния растеж на data.table + manual. Разликата е почти два порядъка!
Ако знаете, че ще добавите доста малък брой редове (n<=100), продължете напред и използвайте най-простото възможно решение: просто присвоете редовете към data.frame, като използвате запис в скоби, и пренебрегнете факта, че data.frame не е предварително попълнена.
За всичко останало използвайте data.table::set
и увеличавайте таблицата data.table по експоненциален път (например с помощта на моя код).