[R] performance of do.call("rbind")

Jeff Newmiller jdnewmil at dcn.davis.ca.us
Tue Jun 28 05:34:53 CEST 2016


Sarah, you make it sound as though everyone should be using matrices, even 
though they have distinct disadvantages for many types of analysis.

You are right that rbind on data frames is slow, but dplyr::bind_rows 
handles data frames almost as fast as your rbind-ing matrices solution.

And if you apply knowledge of your data frames and don't do the error 
checking that bind_rows does, you can beat both of them without converting 
to matrices, as the "tm.dfcolcat" solution below illustrates. (Not for 
everyday use, but if you have a big job and the data are clean this may 
make a difference.)

Data frames, handled properly, are only slightly slower than matrices for 
most purposes. I have seen numerical solutions of partial differential 
equations run lightning fast using pre-allocated data frames and vector 
calculations, so even traditional "matrix" calculation domains don't have 
use matrices to be competitive.

######################
testsize <- 5000
N <- 20

set.seed(1234)
testdf.list <- lapply( seq_len( testsize )
                      , function( x ) {
                         data.frame( matrix( runif( 300 ), nrow=100 ) )
                        }
                      )

tm.rbind <- function( x = 0 ) {
   system.time( r.df <- do.call( "rbind", testdf.list ) )
}
#toss the first one
tm.rbind()
tms.rbind <- data.frame( do.call( rbind
                                 , lapply( 1:N
                                         , tm.rbind
                                         )
                                 )
                        , which = "rbind"
                        )

tm.rbindm <- function( x = 0 ) {
   system.time({
     testm.list <- lapply( testdf.list, as.matrix )
     r.m <- do.call( rbind, testm.list )
   })
}
#toss the first one
tm.rbindm()
tms.rbindm <- data.frame( do.call( rbind
                                  , lapply( 1:N
                                          , tm.rbindm
                                          )
                                  )
                         , which = "rbindm"
                         )

tm.dfcopy <- function(x=0) {
   system.time({
     l.df <- data.frame( matrix( NA
                               , nrow=100 * testsize
                               , ncol=3
                               )
                       )
     for ( i in seq_len( testsize ) ) {
       start <- ( i - 1 ) * 100 + 1
       end <- i * 100
       l.df[ start:end, ] <- testdf.list[[ i ]]
     }
   })
}
#toss the first one
tm.dfcopy()
tms.dfcopy <- data.frame( do.call( rbind
                                  , lapply( 1:N
                                          , tm.dfcopy
                                          )
                                  )
                         , which = "dfcopy"
                         )

tm.dfmatcopy <- function(x=0) {
   system.time({
     l.m <- data.frame( matrix( NA
                              , nrow=100 * testsize
                              , ncol = 3
                              )
                      )
     testm.list <- lapply( testdf.list, as.matrix )
     for ( i in seq_len( testsize ) ) {
       start <- ( i - 1 ) * 100 + 1
       end <- i * 100
       l.m[ start:end, ] <- testm.list[[ i ]]
     }
   })
}
#toss the first one
tm.dfmatcopy()
tms.dfmatcopy <- data.frame( do.call( rbind
                                     , lapply( 1:N
                                             , tm.dfmatcopy
                                             )
                                     )
                            , which = "dfmatcopy"
                            )

tm.bind_rows <- function(x=0) {
   system.time({
     dplyr::bind_rows( testdf.list )
   })
}
#toss the first one
tm.bind_rows()
tms.bind_rows <- data.frame( do.call( rbind
                                     , lapply( 1:N
                                             , tm.bind_rows
                                             )
                                     )
                            , which = "bind_rows"
                            )

tm.dfcolcat <- function(x=0) {
   system.time({
     mycolnames <- names( testdf.list[[ 1 ]] )
     result <-
       setNames( data.frame( lapply( mycolnames
                                   , function( colidx ) {
                                       do.call( c
                                              , lapply( testdf.list
                                                      , function( v ) {
                                                          v[[ colidx ]]
                                                        }
                                                      )
                                              )
                                     }
                                   )
                           )
               , mycolnames
               )
       })
}
#toss the first one
tm.dfcolcat()
tms.dfcolcat <- data.frame( do.call( rbind, lapply( 1:N
                                                   , tm.dfcolcat
                                                   )
                                    )
                           , which = "dfcolcat"
                           )

tms.sarah <- read.table( text=
"   user  system elapsed  which
   34.280   0.009  34.317  tm.rbind
    0.310   0.000   0.311  tm.rbindm
   81.890   0.069  82.162  tm.dfcopy
   67.664   0.047  68.009  tm.dfmatcopy
", header = TRUE, as.is=TRUE )
mergetms <- rbind( tms.rbind
                  , tms.rbindm
                  , tms.dfcopy
                  , tms.dfmatcopy
                  , tms.bind_rows
                  , tms.dfcolcat
                  )
mergetms$which <- factor( mergetms$which
                         , levels = c( "rbind"
                                     , "rbindm"
                                     , "dfcopy"
                                     , "dfmatcopy"
                                     , "bind_rows"
                                     , "dfcolcat"
                                     )
                         )
plot( user.self ~ which, data=mergetms )
plot( user.self ~ which, data=mergetms, ylim=c(0,4) )

summary( tms.rbind )
#   user.self        sys.self         elapsed        user.child    sys.child
# Min.   :18.84   Min.   :0.0000   Min.   :18.92   Min.   : NA   Min.   : NA
# 1st Qu.:20.83   1st Qu.:0.0275   1st Qu.:20.96   1st Qu.: NA   1st Qu.: NA
# Median :22.91   Median :0.0400   Median :23.00   Median : NA   Median : NA
# Mean   :25.06   Mean   :0.0430   Mean   :25.21   Mean   :NaN   Mean   :NaN
# 3rd Qu.:24.29   3rd Qu.:0.0600   3rd Qu.:24.39   3rd Qu.: NA   3rd Qu.: NA
# Max.   :39.36   Max.   :0.1000   Max.   :39.94   Max.   : NA   Max.   : NA
#                                                  NA's   :20    NA's   :20

summary( tms.rbindm )
#   user.self         sys.self    elapsed         user.child    sys.child
# Min.   :0.2200   Min.   :0   Min.   :0.2200   Min.   : NA   Min.   : NA
# 1st Qu.:0.5600   1st Qu.:0   1st Qu.:0.5800   1st Qu.: NA   1st Qu.: NA
# Median :0.5850   Median :0   Median :0.5900   Median : NA   Median : NA
# Mean   :0.5465   Mean   :0   Mean   :0.5555   Mean   :NaN   Mean   :NaN
# 3rd Qu.:0.5900   3rd Qu.:0   3rd Qu.:0.5925   3rd Qu.: NA   3rd Qu.: NA
# Max.   :0.6100   Max.   :0   Max.   :0.6100   Max.   : NA   Max.   : NA
#                                               NA's   :20    NA's   :20

summary( tms.dfcopy )
#   user.self        sys.self         elapsed        user.child    sys.child
# Min.   :114.2   Min.   :0.0000   Min.   :114.3   Min.   : NA   Min.   : NA
# 1st Qu.:122.7   1st Qu.:0.0000   1st Qu.:123.0   1st Qu.: NA   1st Qu.: NA
# Median :128.3   Median :0.0050   Median :128.4   Median : NA   Median : NA
# Mean   :134.5   Mean   :0.0185   Mean   :134.8   Mean   :NaN   Mean   :NaN
# 3rd Qu.:134.7   3rd Qu.:0.0325   3rd Qu.:134.8   3rd Qu.: NA   3rd Qu.: NA
# Max.   :261.5   Max.   :0.0800   Max.   :263.4   Max.   : NA   Max.   : NA
#                                                  NA's   :20    NA's   :20

summary( tms.dfmatcopy )
#   user.self         sys.self         elapsed        user.child    sys.child
# Min.   : 98.15   Min.   : 0.050   Min.   :102.0   Min.   : NA   Min.   : NA
# 1st Qu.:136.47   1st Qu.: 3.495   1st Qu.:144.6   1st Qu.: NA   1st Qu.: NA
# Median :147.53   Median : 7.135   Median :158.3   Median : NA   Median : NA
# Mean   :177.10   Mean   : 7.030   Mean   :185.2   Mean   :NaN   Mean   :NaN
# 3rd Qu.:159.12   3rd Qu.:10.932   3rd Qu.:166.9   3rd Qu.: NA   3rd Qu.: NA
# Max.   :362.95   Max.   :16.100   Max.   :364.3   Max.   : NA   Max.   : NA
#                                                   NA's   :20    NA's

summary( tms.bind_rows )
#   user.self         sys.self    elapsed         user.child    sys.child
# Min.   :0.8200   Min.   :0   Min.   :0.8200   Min.   : NA   Min.   : NA
# 1st Qu.:0.8300   1st Qu.:0   1st Qu.:0.8375   1st Qu.: NA   1st Qu.: NA
# Median :0.8400   Median :0   Median :0.8400   Median : NA   Median : NA
# Mean   :0.8460   Mean   :0   Mean   :0.8480   Mean   :NaN   Mean   :NaN
# 3rd Qu.:0.8525   3rd Qu.:0   3rd Qu.:0.8525   3rd Qu.: NA   3rd Qu.: NA
# Max.   :0.9400   Max.   :0   Max.   :0.9900   Max.   : NA   Max.   : NA
#                                               NA's   :20    NA's   :20

summary( tms.dfcolcat )
# user.self        sys.self    elapsed        user.child    sys.child
# Min.   :0.340   Min.   :0   Min.   :0.340   Min.   : NA   Min.   : NA
# 1st Qu.:0.350   1st Qu.:0   1st Qu.:0.350   1st Qu.: NA   1st Qu.: NA
# Median :0.360   Median :0   Median :0.360   Median : NA   Median : NA
# Mean   :0.358   Mean   :0   Mean   :0.357   Mean   :NaN   Mean   :NaN
# 3rd Qu.:0.360   3rd Qu.:0   3rd Qu.:0.360   3rd Qu.: NA   3rd Qu.: NA
# Max.   :0.380   Max.   :0   Max.   :0.380   Max.   : NA   Max.   : NA
#                                             NA's   :20    NA's   :20


######################

On Mon, 27 Jun 2016, Sarah Goslee wrote:

> That's not what I said, though, and it's not necessarily true. Growing
> an object within a loop _is_ a slow process, but that's not the
> problem here. The problem is using data frames instead of matrices.
> The need to manage column classes is very costly. Converting to
> matrices will almost always be enormously faster.
>
> Here's an expansion of the previous example I posted, in four parts:
> 1. do.call with data frame - very slow - 34.317 s elapsed time for
> 2000 data frames
> 2. do.call with matrix - very fast - 0.311 s elapsed
> 3. pre-allocated loop with data frame - even slower (!) - 82.162 s
> 4. pre-allocated loop with matrix - very fast - 68.009 s
>
> It matters whether the columns are converted to numeric or character,
> and the time doesn't scale linearly with list length. For a particular
> problem, the best solution may vary greatly (and I didn't even include
> packages beyond the base functionality). In general, though, using
> matrices is faster than using data frames, and using do.call is faster
> than using a pre-allocated loop, which is much faster than growing an
> object.
>
> Sarah
>
>> testsize <- 5000
>>
>> set.seed(1234)
>> testdf <- data.frame(matrix(runif(300), nrow=100, ncol=3))
>> testdf.list <- lapply(seq_len(testsize), function(x)testdf)
>>
>> system.time(r.df <- do.call("rbind", testdf.list))
>   user  system elapsed
> 34.280   0.009  34.317
>>
>> system.time({
> + testm.list <- lapply(testdf.list, as.matrix)
> + r.m <- do.call("rbind", testm.list)
> + })
>   user  system elapsed
>  0.310   0.000   0.311
>>
>> system.time({
> + l.df <- data.frame(matrix(NA, nrow=100 * testsize, ncol=3))
> + for(i in seq_len(testsize)) {
> + start <- (i-1)*100 + 1
> + end <- i*100
> + l.df[start:end, ] <- testdf.list[[i]]
> + }
> + })
>   user  system elapsed
> 81.890   0.069  82.162
>>
>> system.time({
> + l.m <- data.frame(matrix(NA, nrow=100 * testsize, ncol=3))
> + testm.list <- lapply(testdf.list, as.matrix)
> + for(i in seq_len(testsize)) {
> + start <- (i-1)*100 + 1
> + end <- i*100
> + l.m[start:end, ] <- testm.list[[i]]
> + }
> + })
>   user  system elapsed
> 67.664   0.047  68.009
>
>
>
>
> On Mon, Jun 27, 2016 at 1:05 PM, Marc Schwartz <marc_schwartz at me.com> wrote:
>> Hi,
>>
>> Just to add my tuppence, which might not even be worth that these days...
>>
>> I found the following blog post from 2013, which is likely dated to some extent, but provided some benchmarks for a few methods:
>>
>>   http://rcrastinate.blogspot.com/2013/05/the-rbinding-race-for-vs-docall-vs.html
>>
>> There is also a comment with a reference there to using the data.table package, which I don't use, but may be something to evaluate.
>>
>> As Bert and Sarah hinted at, there is overhead in taking the repetitive piecemeal approach.
>>
>> If all of your data frames are of the exact same column structure (column order, column types), it may be prudent to do your own pre-allocation of a data frame that is the target row total size and then "insert" each "sub" data frame by using row indexing into the target structure.
>>
>> Regards,
>>
>> Marc Schwartz
>>
>>
>>> On Jun 27, 2016, at 11:54 AM, Witold E Wolski <wewolski at gmail.com> wrote:
>>>
>>> Hi Bert,
>>>
>>> You are most likely right. I just thought that do.call("rbind", is
>>> somehow more clever and allocates the memory up front. My error. After
>>> more searching I did find rbind.fill from plyr which seems to do the
>>> job (it computes the size of the result data.frame and allocates it
>>> first).
>>>
>>> best
>>>
>>> On 27 June 2016 at 18:49, Bert Gunter <bgunter.4567 at gmail.com> wrote:
>>>> The following might be nonsense, as I have no understanding of R
>>>> internals; but ....
>>>>
>>>> "Growing" structures in R by iteratively adding new pieces is often
>>>> warned to be inefficient when the number of iterations is large, and
>>>> your rbind() invocation might fall under this rubric. If so, you might
>>>> try  issuing the call say, 20 times, over 10k disjoint subsets of the
>>>> list, and then rbinding up the 20 large frames.
>>>>
>>>> Again, caveat emptor.
>>>>
>>>> Cheers,
>>>> Bert
>>>>
>>>>
>>>> Bert Gunter
>>>>
>>>> "The trouble with having an open mind is that people keep coming along
>>>> and sticking things into it."
>>>> -- Opus (aka Berkeley Breathed in his "Bloom County" comic strip )
>>>>
>>>>
>>>> On Mon, Jun 27, 2016 at 8:51 AM, Witold E Wolski <wewolski at gmail.com> wrote:
>>>>> I have a list (variable name data.list) with approx 200k data.frames
>>>>> with dim(data.frame) approx 100x3.
>>>>>
>>>>> a call
>>>>>
>>>>> data <-do.call("rbind", data.list)
>>>>>
>>>>> does not complete - run time is prohibitive (I killed the rsession
>>>>> after 5 minutes).
>>>>>
>>>>> I would think that merging data.frame's is a common operation. Is
>>>>> there a better function (more performant) that I could use?
>>>>>
>>>>> Thank you.
>>>>> Witold
>>>>>
>
> ______________________________________________
> R-help at r-project.org mailing list -- To UNSUBSCRIBE and more, see
> https://stat.ethz.ch/mailman/listinfo/r-help
> PLEASE do read the posting guide http://www.R-project.org/posting-guide.html
> and provide commented, minimal, self-contained, reproducible code.
>

---------------------------------------------------------------------------
Jeff Newmiller                        The     .....       .....  Go Live...
DCN:<jdnewmil at dcn.davis.ca.us>        Basics: ##.#.       ##.#.  Live Go...
                                       Live:   OO#.. Dead: OO#..  Playing
Research Engineer (Solar/Batteries            O.O#.       #.O#.  with
/Software/Embedded Controllers)               .OO#.       .OO#.  rocks...1k



More information about the R-help mailing list