Почему sapply относительно медленно при запросе атрибутов на переменные в data.frame?

Что-то меня удивило: давайте сравним два способа получения class es для переменных в большом кадре данных со многими столбцами: решение sapply и решение цикла for.

bigDF <- as.data.frame( matrix( 0, nrow=1E5, ncol=1E3 ) )
library( microbenchmark )

for_soln <- function(x) {
  out <- character( ncol(x) )
  for( i in 1:ncol(x) ) {
    out[i] <- class(x[,i])
  }
  return( out )
}

microbenchmark( times=20,
  sapply( bigDF, class ),
  for_soln( bigDF )
)

дает мне, на моей машине,

Unit: milliseconds
                  expr       min        lq    median       uq      max
1      for_soln(bigDF)  21.26563  21.58688  26.03969 163.6544 300.6819
2 sapply(bigDF, class) 385.90406 405.04047 444.69212 471.8829 889.6217

Интересно, что если мы преобразуем bigDF в список, sapply снова будет приятным и быстрым.

bigList <- as.list( bigDF )
for_soln2 <- function(x) {
  out <- character( length(x) )
  for( i in 1:length(x) ) {
    out[i] <- class( x[[i]] )
  }
  return( out )
}

microbenchmark( sapply( bigList, class ), for_soln2( bigList ) )

дает мне

Unit: milliseconds
                    expr      min       lq   median       uq      max
1     for_soln2(bigList) 1.887353 1.959856 2.010270 2.058968 4.497837
2 sapply(bigList, class) 1.348461 1.386648 1.401706 1.428025 3.825547

Почему эти операции, особенно sapply, занимают намного больше времени с data.frame по сравнению с list? И есть ли более идиоматическое решение?

Ответ 1

edit: Старое предложенное решение t3 <- sapply(1:ncol(bigDF), function(idx) class(bigDF[,idx])) теперь изменено на t3 <- sapply(1:ncol(bigDF), function(idx) class(bigDF[[idx]])). Это еще быстрее. Благодаря комментарию @Wojciech

Причина, по которой я могу думать, заключается в том, что вы без необходимости конвертируете data.frame в список. Кроме того, ваши результаты также не идентичны.

bigDF <- as.data.frame(matrix(0, nrow=1E5, ncol=1E3))
t1 <- sapply(bigDF, class)
t2 <- for_soln(bigDF)

> head(t1)
    V1        V2        V3        V4        V5        V6 
"numeric" "numeric" "numeric" "numeric" "numeric" "numeric" 
> head(t2)
[1] "numeric" "numeric" "numeric" "numeric" "numeric" "numeric"

> identical(t1, t2)
[1] FALSE

Выполнение Rprof on sapply говорит, что все потраченное время находится на as.list.data.fraame

Rprof()
t1 <- sapply(bigDF, class)
Rprof(NULL)
summaryRprof()

$by.self
                     self.time self.pct total.time total.pct
"as.list.data.frame"      1.16      100       1.16       100    

Вы можете ускорить операцию, не спрашивая as.list.data.frame. Вместо этого мы могли просто запросить класс каждого столбца data.frame напрямую, как показано ниже. Это в точности эквивалентно тому, что вы делаете с for-loop на самом деле.

t3 <- sapply(1:ncol(bigDF), function(idx) class(bigDF[[idx]]))
> identical(t2, t3)
[1] TRUE

microbenchmark(times=20, 
    sapply(bigDF, class),
    for_soln(bigDF),
    sapply(1:ncol(bigDF), function(idx) 
        class(bigDF[[idx]]))
)

Unit: milliseconds
        expr             min        lq       median       uq       max
1   for-soln (t2)     38.31545   39.45940   40.48152   43.05400  313.9484
2   sapply-new (t3)   18.51510   18.82293   19.87947   26.10541  261.5233
3   sapply-orig (t1) 952.94612 1075.38915 1159.49464 1204.52747 1484.1522

Разница в t3 заключается в том, что вы создаете список длиной 1000 каждый с длиной 1. В то время как в t1 его список длиной 1000, каждый длиной 10000.