Является ли R применимым к семейству больше, чем синтаксический сахар?

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

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

Ответ 1

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

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

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

Edit:

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

> 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 или <<-, и в этом случае вы можете ввести побочные эффекты). Излишне говорить, что важно быть осторожным в отношении локальных и глобальных переменных, особенно при параллельном выполнении.

Edit:

Вот тривиальный пример, демонстрирующий разницу между 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.

Ответ 2

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

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 × 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 

Там вы идете. Что я выиграл?; -)

Ответ 3

... и, как я только что написал в другом месте, 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 

Ответ 4

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

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 вряд ли повлияет на вещи)

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

Взяв пример Joris Meys, где он заменяет традиционную для цикла удобную функцию 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 Joris Meys на каждой итерации происходит множество (7?) Операций, и для ее выполнения достаточно немного настроек. Заметьте также, насколько это компактнее, чем версия for.

Ответ 5

При применении функций над подмножествами вектора 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