Hľadal som na StackOverflow, ale nemôžem nájsť riešenie špecifické pre môj problém, ktorý zahŕňa pridávanie riadkov do dátového rámca R.
Inicializujem prázdny dvojstĺpcový dátový rámec takto.
df = data.frame(x = numeric(), y = character())
Potom je mojím cieľom iterovať cez zoznam hodnôt a v každej iterácii pridať hodnotu na koniec zoznamu. Začal som s nasledujúcim kódom.
for (i in 1:10) {
df$x = rbind(df$x, i)
df$y = rbind(df$y, toString(i))
}
Skúsil som aj funkcie c
, append
a merge
, ale bez úspechu. Dajte mi prosím vedieť, ak máte nejaké návrhy.
Keďže neviem, o čo sa snažíte, podelím sa s vami ešte o jeden návrh: Vložte hodnoty do týchto vektorov a potom na konci vytvorte svoj data.frame
.
Pokračovanie Julianovho f3
(predalokovaný data.frame
) ako doteraz najrýchlejšia možnosť, definovaná ako:
# 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
}
Tu'je podobný prístup, ale taký, kde sa data.frame
vytvára ako posledný krok.
# 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
z balíka "microbenchmark" nám poskytne komplexnejší pohľad ako 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()
(prístup uvedený nižšie) je neuveriteľne neefektívny kvôli tomu, ako často volá data.frame
, a kvôli tomu, že rast objektov týmto spôsobom je v R všeobecne pomalý. f3()
je oveľa lepší vďaka predalokácii, ale samotná štruktúra data.frame
tu môže byť súčasťou úzkych miest. Funkcia f4()
sa snaží toto úzke miesto obísť bez toho, aby bol ohrozený prístup, ktorý chcete použiť.
Toto naozaj nie je dobrý nápad, ale ak ste to chceli urobiť týmto spôsobom, myslím, že to môžete skúsiť:
for (i in 1:10) {
df <- rbind(df, data.frame(x = i, y = toString(i)))
}
Všimnite si, že vo vašom kóde je ešte jeden problém:
stringsAsFactors
. Použite: df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
Porovnajme tri navrhované riešenia:
# 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
Najlepším riešením je predbežné pridelenie priestoru (podľa zámeru v R). Ďalším najlepším riešením je použitie list
a najhorším riešením (aspoň na základe týchto výsledkov časovania) sa zdá byť rbind
.
Predpokladajme, že jednoducho vopred nepoznáte veľkosť data.frame. Môže to byť niekoľko riadkov alebo niekoľko miliónov. Musíte mať nejaký kontajner, ktorý dynamicky rastie. Vzhľadom na moje skúsenosti a všetky súvisiace odpovede v SO som prišiel so 4 rôznymi riešeniami:
rbindlist
do data.frame
V prípade potreby použite rýchlu operáciu data.table
'a spojte ju s ručným zdvojením tabuľky.
Použite RSQLite
a pridajte do tabuľky uchovávanej v pamäti.
data.frame
'vlastná schopnosť rásť a použiť vlastné prostredie (ktoré má referenčnú sémantiku) na uloženie data.frame, aby sa pri návrate nekopíroval.
Tu je test všetkých metód pre malý aj veľký počet pripojených riadkov. Ku každej metóde sú priradené 3 funkcie:
create(first_element)
, ktorá vráti príslušný podkladový objekt s vloženým first_element
.
append(object, element)
, ktorá pripojí element
na koniec tabuľky (reprezentovaný objektom
).
access(object)
získa data.frame
so všetkými vloženými prvkami.
rbindlist
do data.frameTo je celkom jednoduché a priamočiare:
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
+ ručné zdvojenie tabuľky v prípade potreby.Skutočnú dĺžku tabuľky uložím do atribútu 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
Toto je v podstate copy&paste odpovede Karsten W. answer v podobnom vlákne.
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
's vlastným riadkovaním + vlastné prostredie.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)
}
Pre pohodlie použijem jednu testovaciu funkciu, ktorá pokryje všetky s nepriamym volaním. (Overil som si to: použitie do.call
namiesto priameho volania funkcií nespôsobí, že by kód bežal merateľne dlhšie).
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)))
}
Pozrime sa na výkon pre n=10 vkladaní.
Pridal som aj 'placebo' funkcie (s príponou 0
), ktoré nič nevykonávajú - len na zmeranie réžie testovacieho nastavenia.
r<-microbenchmark(test(0,n=10), test(1,n=10),test(2,n=10),test(3,n=10), test(4,n=10))
autoplot(r)
Pre 1E5 riadkov (merania vykonané na procesore Intel(R) Core(TM) i7-4710HQ @ 2,50 GHz):
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
Vyzerá to tak, že sulution založený na SQLite síce získava určitú rýchlosť pri veľkých údajoch, ale ani zďaleka sa nepribližuje k data.table + manuálnemu exponenciálnemu rastu. Rozdiel je takmer dva rády!
Ak viete, že budete pripájať pomerne malý počet riadkov (n<=100), pokojne použite najjednoduchšie možné riešenie: jednoducho priraďte riadky do data.frame pomocou zápisu v zátvorkách a ignorujte skutočnosť, že data.frame nie je predvyplnený.
Na všetko ostatné použite data.table::set
a exponenciálne zväčšujte data.table (napr. pomocou môjho kódu).