Семейство R's больше, чем синтаксический сахар? - PullRequest
144 голосов
/ 16 февраля 2010

... относительно времени выполнения и / или памяти.

Если это не так, докажите это с помощью фрагмента кода. Обратите внимание, что ускорение векторизацией не считается. Ускорение должно происходить из apply (tapply, sapply, ...).

Ответы [ 5 ]

146 голосов
/ 16 февраля 2010

Функции apply в R не обеспечивают улучшенную производительность по сравнению с другими функциями зацикливания (например, for).Единственное исключение из этого - lapply, которое может быть немного быстрее, поскольку в коде C он выполняет больше работы, чем в R (см. этот вопрос для примера этого ).

Но в целом правило: , вы должны использовать функцию применения для ясности, а не для производительности .

Я бы добавил к этому, что применяемые функции не имеют никаких побочных эффектов , что является важным отличием, когда речь идет о функциональном программировании с использованием RЭто может быть отменено с помощью assign или <<-, но это может быть очень опасно.Побочные эффекты также затрудняют понимание программы, поскольку состояние переменной зависит от истории.

Редактировать:

Просто для того, чтобы подчеркнуть это тривиальным примером, который рекурсивно вычисляетПоследовательность Фибоначчи;это может быть выполнено несколько раз для получения точного измерения, но дело в том, что ни один из методов не имеет существенно отличающихся характеристик:

> fibo <- function(n) {
+   if ( n < 2 ) n
+   else fibo(n-1) + fibo(n-2)
+ }
> system.time(for(i in 0:26) fibo(i))
   user  system elapsed 
   7.48    0.00    7.52 
> system.time(sapply(0:26, fibo))
   user  system elapsed 
   7.50    0.00    7.54 
> system.time(lapply(0:26, fibo))
   user  system elapsed 
   7.48    0.04    7.54 
> library(plyr)
> system.time(ldply(0:26, fibo))
   user  system elapsed 
   7.52    0.00    7.58 

Редактировать 2:

Что касается использования параллельных пакетов для R (например, rpvm, rmpi, snow), они обычно предоставляют семейные функции apply (даже пакет foreach по сути эквивалентен, несмотря на название).Вот простой пример функции sapply в snow:

library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)

В этом примере используется кластер сокетов, для которого не требуется устанавливать дополнительное программное обеспечение;в противном случае вам понадобится что-то вроде PVM или MPI (см. Страница кластеризации Tierney ).snow имеет следующие применяемые функции:

parLapply(cl, x, fun, ...)
parSapply(cl, X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
parApply(cl, X, MARGIN, FUN, ...)
parRapply(cl, x, fun, ...)
parCapply(cl, x, fun, ...)

Имеет смысл, что apply функции должны использоваться для параллельного выполнения, так как они не имеют побочных эффектов .Когда вы изменяете значение переменной в цикле for, оно устанавливается глобально.С другой стороны, все функции apply можно безопасно использовать параллельно, поскольку изменения являются локальными для вызова функции (если только вы не попытаетесь использовать assign или <<-, в этом случае вы можете ввести побочные эффекты).Само собой разумеется, важно быть осторожным с локальными и глобальными переменными, особенно когда имеешь дело с параллельным выполнением.

Редактировать:

Вот тривиальный пример, чтобы продемонстрироватьразница между for и *apply в отношении побочных эффектов:

> df <- 1:10
> # *apply example
> lapply(2:3, function(i) df <- df * i)
> df
 [1]  1  2  3  4  5  6  7  8  9 10
> # for loop example
> for(i in 2:3) df <- df * i
> df
 [1]  6 12 18 24 30 36 42 48 54 60

Обратите внимание, как df в родительской среде изменяется на for, но не *apply.

69 голосов
/ 27 августа 2010

Иногда ускорение может быть значительным, например, когда вам приходится вкладывать циклы for, чтобы получить среднее значение на основе группировки из более чем одного фактора. Здесь у вас есть два подхода, которые дают одинаковый результат:

set.seed(1)  #for reproducability of the results

# The data
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))

# the function forloop that averages X over every combination of Y and Z
forloop <- function(x,y,z){
# These ones are for optimization, so the functions 
#levels() and length() don't have to be called more than once.
  ylev <- levels(y)
  zlev <- levels(z)
  n <- length(ylev)
  p <- length(zlev)

  out <- matrix(NA,ncol=p,nrow=n)
  for(i in 1:n){
      for(j in 1:p){
          out[i,j] <- (mean(x[y==ylev[i] & z==zlev[j]]))
      }
  }
  rownames(out) <- ylev
  colnames(out) <- zlev
  return(out)
}

# Used on the generated data
forloop(X,Y,Z)

# The same using tapply
tapply(X,list(Y,Z),mean)

Оба дают одинаковый результат, будучи матрицей 5 x 10 со средними значениями и именованными строками и столбцами. Но:

> system.time(forloop(X,Y,Z))
   user  system elapsed 
   0.94    0.02    0.95 

> system.time(tapply(X,list(Y,Z),mean))
   user  system elapsed 
   0.06    0.00    0.06 

Вот, пожалуйста. Что я выиграл? ; -)

45 голосов
/ 24 марта 2011

... и как я только что написал в другом месте, vapply твой друг! ... это как sapply, но вы также указываете тип возвращаемого значения, что делает его намного быстрее.

> system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
   user  system elapsed 
   3.54    0.00    3.53 
> system.time(z <- lapply(y, foo))
   user  system elapsed 
   2.89    0.00    2.91 
> system.time(z <- vapply(y, foo, numeric(1)))
   user  system elapsed 
   1.35    0.00    1.36 
27 голосов
/ 02 февраля 2011

Я писал в другом месте, что пример, подобный Шейну, на самом деле не подчеркивает разницу в производительности между различными типами циклического синтаксиса, потому что все время затрачивается внутри функции, а не на нагрузку на цикл. Кроме того, код несправедливо сравнивает цикл for без памяти с функциями семейства apply, которые возвращают значение. Вот немного другой пример, который подчеркивает точку.

foo <- function(x) {
   x <- x+1
 }
y <- numeric(1e6)
system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
#   user  system elapsed 
#  4.967   0.049   7.293 
system.time(z <- sapply(y, foo))
#   user  system elapsed 
#  5.256   0.134   7.965 
system.time(z <- lapply(y, foo))
#   user  system elapsed 
#  2.179   0.126   3.301 

Если вы планируете сохранить результат, то применение семейных функций может быть намного больше, чем синтаксический сахар.

(простой список неопределяемых значений z составляет всего 0,2 с, поэтому задержка выполняется намного быстрее. Инициализация z в цикле for выполняется довольно быстро, потому что я даю среднее значение за последние 5 из 6 запусков, перемещая его за пределы системы. .time вряд ли повлияет на вещи)

Еще одна вещь, которую следует отметить, это то, что есть еще одна причина использовать семейные функции независимо от их производительности, ясности или отсутствия побочных эффектов. Цикл for обычно способствует тому, чтобы как можно больше помещать в цикл. Это связано с тем, что каждый цикл требует настройки переменных для хранения информации (среди других возможных операций). Применить заявления, как правило, смещены в другую сторону. Часто вы хотите выполнить несколько операций с вашими данными, некоторые из которых могут быть векторизованы, но некоторые могут быть не в состоянии. В R, в отличие от других языков, лучше отделить те операции и запустить те, которые не векторизованы в операторе применения (или векторизованной версии функции), и те, которые векторизованы как истинные векторные операции. Это часто значительно повышает производительность.

На примере Joris Meys, где он заменяет традиционный цикл for на удобную функцию R, мы можем использовать его, чтобы показать эффективность написания кода более дружественным для R способом для аналогичного ускорения без специализированной функции.

set.seed(1)  #for reproducability of the results

# The data - copied from Joris Meys answer
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))

# an R way to generate tapply functionality that is fast and 
# shows more general principles about fast R coding
YZ <- interaction(Y, Z)
XS <- split(X, YZ)
m <- vapply(XS, mean, numeric(1))
m <- matrix(m, nrow = length(levels(Y)))
rownames(m) <- levels(Y)
colnames(m) <- levels(Z)
m

Получается намного быстрее, чем цикл for, и чуть медленнее, чем встроенная оптимизированная функция tapply. Это не потому, что vapply намного быстрее, чем for, а потому, что он выполняет только одну операцию в каждой итерации цикла. В этом коде все остальное векторизовано. В традиционном цикле for Джориса Мейса в каждой итерации выполняется много (7?) Операций, и для его выполнения требуется немало настроек. Также обратите внимание, насколько она компактнее, чем версия for.

3 голосов
/ 10 апреля 2013

При применении функций к подмножествам вектора, tapply может быть довольно быстрым, чем цикл for. Пример:

df <- data.frame(id = rep(letters[1:10], 100000),
                 value = rnorm(1000000))

f1 <- function(x)
  tapply(x$value, x$id, sum)

f2 <- function(x){
  res <- 0
  for(i in seq_along(l <- unique(x$id)))
    res[i] <- sum(x$value[x$id == l[i]])
  names(res) <- l
  res
}            

library(microbenchmark)

> microbenchmark(f1(df), f2(df), times=100)
Unit: milliseconds
   expr      min       lq   median       uq      max neval
 f1(df) 28.02612 28.28589 28.46822 29.20458 32.54656   100
 f2(df) 38.02241 41.42277 41.80008 42.05954 45.94273   100

apply, однако, в большинстве случаев не обеспечивает увеличения скорости, а в некоторых случаях может быть даже намного медленнее:

mat <- matrix(rnorm(1000000), nrow=1000)

f3 <- function(x)
  apply(x, 2, sum)

f4 <- function(x){
  res <- 0
  for(i in 1:ncol(x))
    res[i] <- sum(x[,i])
  res
}

> microbenchmark(f3(mat), f4(mat), times=100)
Unit: milliseconds
    expr      min       lq   median       uq      max neval
 f3(mat) 14.87594 15.44183 15.87897 17.93040 19.14975   100
 f4(mat) 12.01614 12.19718 12.40003 15.00919 40.59100   100

Но для этих ситуаций у нас есть colSums и rowSums:

f5 <- function(x)
  colSums(x) 

> microbenchmark(f5(mat), times=100)
Unit: milliseconds
    expr      min       lq   median       uq      max neval
 f5(mat) 1.362388 1.405203 1.413702 1.434388 1.992909   100
...