Почему метод apply() медленнее, чем цикл for в R?
В качестве передового опыта я пытаюсь определить, лучше ли создавать функцию и apply()
через матрицу, или лучше просто процитировать матрицу через функцию. Я попробовал это в обоих направлениях и был удивлен, обнаружив, что apply()
работает медленнее. Задача состоит в том, чтобы взять вектор и оценить его как положительный или отрицательный, а затем вернуть вектор с 1, если он положительный и -1, если он отрицательный. Циклы функций mash()
и функция squish()
передаются функции apply()
.
million <- as.matrix(rnorm(100000))
mash <- function(x){
for(i in 1:NROW(x))
if(x[i] > 0)
x[i] <- 1
else
x[i] <- -1
return(x)
}
squish <- function(x){
if(x >0)
return(1)
else
return(-1)
}
ptm <- proc.time()
loop_million <- mash(million)
proc.time() - ptm
ptm <- proc.time()
apply_million <- apply(million,1, squish)
proc.time() - ptm
loop_million
результаты:
user system elapsed
0.468 0.008 0.483
apply_million
результаты:
user system elapsed
1.401 0.021 1.423
В чем преимущество использования apply()
в цикле for
, если производительность ухудшается? Есть ли недостаток в моем тесте? Я сравнил два результирующих объекта для подсказки и нашел:
> class(apply_million)
[1] "numeric"
> class(loop_million)
[1] "matrix"
Это только углубляет тайну. Функция apply()
не может принять простой числовой вектор, и поэтому я начал использовать его с as.matrix()
в начале. Но затем он возвращает числовое значение. Цикл for
хорош с простым числовым вектором. И он возвращает объект того же класса, что и тот, который ему передан.
Ответы
Ответ 1
Как сказал Чейз: используйте силу векторизации. Вы сравниваете два плохих решения здесь.
Чтобы выяснить, почему ваше прикладное решение работает медленнее:
В цикле for вы фактически используете векторизованные индексы матрицы, что означает, что преобразование типа не происходит. Я немного грубо разбираюсь с этим здесь, но в основном внутренний метод расчета игнорирует размеры. Они просто сохраняются как атрибут и возвращаются с вектором, представляющим матрицу. Чтобы проиллюстрировать:
> x <- 1:10
> attr(x,"dim") <- c(5,2)
> y <- matrix(1:10,ncol=2)
> all.equal(x,y)
[1] TRUE
Теперь, когда вы используете apply, матрица разбивается внутренне на 100 000 векторов строк, каждый вектор строки (т.е. один номер) помещается через функцию, и в итоге результат объединяется в соответствующую форму. Функция apply считает, что вектор лучше в этом случае и, следовательно, должен конкатенировать результаты всех строк. Это требует времени.
Также функция sapply сначала использует as.vector(unlist(...))
для преобразования чего-либо в вектор и в конце пытается упростить ответ в подходящую форму. Кроме того, это требует времени, следовательно, также может быть медленнее. Тем не менее, это не на моей машине.
IF apply будет решением здесь (и это не так), вы можете сравнить:
> system.time(loop_million <- mash(million))
user system elapsed
0.75 0.00 0.75
> system.time(sapply_million <- matrix(unlist(sapply(million,squish,simplify=F))))
user system elapsed
0.25 0.00 0.25
> system.time(sapply2_million <- matrix(sapply(million,squish)))
user system elapsed
0.34 0.00 0.34
> all.equal(loop_million,sapply_million)
[1] TRUE
> all.equal(loop_million,sapply2_million)
[1] TRUE
Ответ 2
Точка приложения (и plyr) - это не скорость, а выразительность. Они также имеют тенденцию предотвращать ошибки, потому что они устраняют код хранения книг, необходимый с помощью циклов.
В последнее время ответы на stackoverflow имеют слишком высокую скорость. Ваш код будет работать быстрее, поскольку компьютеры будут быстрее, а R-core оптимизирует внутренние элементы R. Ваш код никогда не станет более изящным или более понятным сам по себе.
В этом случае вы можете получить лучшее из обоих миров: элегантный ответ с использованием векторизации, который также очень быстр, (million > 0) * 2 - 1
.
Ответ 3
Вы можете использовать lapply
или sapply
на векторах, если хотите. Однако почему бы не использовать соответствующий инструмент для задания, в данном случае ifelse()
?
> ptm <- proc.time()
> ifelse_million <- ifelse(million > 0,1,-1)
> proc.time() - ptm
user system elapsed
0.077 0.007 0.093
> all.equal(ifelse_million, loop_million)
[1] TRUE
И для сравнения, вот два сравниваемых прогона с использованием цикла for и sapply:
> ptm <- proc.time()
> apply_million <- sapply(million, squish)
> proc.time() - ptm
user system elapsed
0.469 0.004 0.474
> ptm <- proc.time()
> loop_million <- mash(million)
> proc.time() - ptm
user system elapsed
0.408 0.001 0.417
Ответ 4
В этом случае гораздо быстрее выполнить замену на основе индекса, чем семейство ifelse()
, *apply()
или цикл:
> million <- million2 <- as.matrix(rnorm(100000))
> system.time(million3 <- ifelse(million > 0, 1, -1))
user system elapsed
0.046 0.000 0.044
> system.time({million2[(want <- million2 > 0)] <- 1; million2[!want] <- -1})
user system elapsed
0.006 0.000 0.007
> all.equal(million2, million3)
[1] TRUE
Хорошо иметь все эти инструменты на кончиках пальцев. Вы можете использовать тот, который имеет для вас наибольший смысл (так как вам нужно понимать код месяцев или лет спустя), а затем начать переходить к более оптимизированным решениям, если время вычисления становится непомерно высоким.
Ответ 5
Лучший пример для преимущества скорости для цикла.
for_loop <- function(x){
out <- vector(mode="numeric",length=NROW(x))
for(i in seq(length(out)))
out[i] <- max(x[i,])
return(out)
}
apply_loop <- function(x){
apply(x,1,max)
}
million <- matrix(rnorm(1000000),ncol=10)
> system.time(apply_loop(million))
user system elapsed
0.57 0.00 0.56
> system.time(for_loop(million))
user system elapsed
0.32 0.00 0.33
ИЗМЕНИТЬ
Версия, предложенная Эдуардо.
max_col <- function(x){
x[cbind(seq(NROW(x)),max.col(x))]
}
По строке
> system.time(for_loop(million))
user system elapsed
0.99 0.00 1.11
> system.time(apply_loop(million))
user system elapsed
1.40 0.00 1.44
> system.time(max_col(million))
user system elapsed
0.06 0.00 0.06
По столбцу
> system.time(for_loop(t(million)))
user system elapsed
0.05 0.00 0.05
> system.time(apply_loop(t(million)))
user system elapsed
0.07 0.00 0.07
> system.time(max_col(t(million)))
user system elapsed
0.04 0.00 0.06