R은 가족을 구문 설탕보다 더 많이 적용합니까?


152

... 실행 시간 및 / 또는 메모리와 관련하여.

이것이 사실이 아닌 경우 코드 스 니펫으로이를 증명하십시오. 벡터화에 의한 속도 향상은 계산되지 않습니다. 속도 향상에서 온해야한다 apply( tapply, sapply, ...) 그 자체.

답변:


152

applyR 의 함수는 다른 루핑 함수 (예 :)보다 향상된 성능을 제공하지 않습니다 for. 이것에 대한 한 가지 예외 lapply는 R보다 C 코드에서 더 많은 작업을 수행하기 때문에 조금 더 빠를 수 있습니다 ( 이 예제는이 질문을 참조하십시오 ).

그러나 일반적으로 성능이 아니라 명확성을 위해 apply 함수를 사용해야합니다 .

나는이에 추가 할 기능이 없다 적용 부작용 은 사용에 의해 오버라이드 (override) 할 수 있습니다 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.


30
R을위한 대부분의 멀티 코어 패키지 apply는 기능 군을 통해 병렬화를 구현 합니다. 따라서 적용 할 수 있도록 프로그램을 구성하면 아주 적은 비용으로 병렬화 할 수 있습니다.
Sharpie

샤피-고마워요! (Windows XP에서)를 보여주는 예제에 대한 아이디어가 있습니까?
탈 Galili

5
snowfall패키지를 보고 비 네트에서 예제를 사용해 보는 것이 좋습니다 . 패키지 snowfall위에 빌드하고 snow병렬화의 세부 사항을 추상화하여 병렬화 된 apply기능 을 실행하는 것이 훨씬 간단 합니다.
Sharpie

1
@ 샤피 (Sharpie) 그러나 그 foreach이후에 사용 가능 해졌으므로 SO에 대해 많이 문의하는 것처럼 보입니다.
Ari B. Friedman

1
@Shane, 답의 맨 위에는 루프 lapply보다 "약간 빠른" 사례의 예로 다른 질문에 연결합니다 for. 그러나 거기에 제안하는 것이 없습니다. 당신 은 다른 것 때문에 잘 알려진 사실 lapply보다 더 빠르다고 언급합니다 sapply( sapply출력을 단순화하려고 시도하므로 많은 데이터 크기 검사 및 잠재적 인 변환을 수행해야 함). 관련이 없습니다 for. 뭔가 빠졌습니까?
flodel

70

여러 요소의 그룹화를 기반으로 평균을 얻기 위해 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 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 

당신은 간다. 내가 무엇을 이겼습니까? ;-)


아아, 너무 달콤 :-) 나는 누군가가 내 대답이 늦었는지 궁금해하고있었습니다.
Joris Meys

1
나는 항상 "활성"으로 정렬합니다. :) 답변을 일반화하는 방법을 잘 모르겠습니다. 때로는 *apply더 빠릅니다. 그러나 더 중요한 점은 부작용 이라고 생각합니다 (예로 대답을 업데이트했습니다).
Shane

1
다른 하위 집합에 함수를 적용하려는 경우 적용이 특히 빠르다고 생각합니다. 중첩 루프에 대한 스마트 적용 솔루션이 있다면 적용 솔루션도 더 빠를 것 같습니다. 대부분의 경우 apply는 속도가 빠르지 않지만 부작용에 동의합니다.
Joris Meys

2
이것은 약간의 주제이지만,이 특정 예에서는 data.table훨씬 빠르며 "더 쉬워"라고 생각합니다. library(data.table) dt<-data.table(X,Y,Z,key=c("Y,Z")) system.time(dt[,list(X_mean=mean(X)),by=c("Y,Z")])
dnlbrky

12
이 비교는 어리 석다. tapply특정 작업을위한 전문 기능이다 더 빨리 for 루프보다 더 이유는. for 루프가 수행 할 수있는 작업은 수행 할 수 없습니다 (일반적인 작업은 수행 apply할 수 있음). 사과와 오렌지를 비교하고 있습니다.
eddi

47

... 방금 다른 곳에 쓴 것처럼, vapply는 당신의 친구입니다! ... 그것은 sapply와 비슷하지만 훨씬 빠르게 만드는 반환 값 유형을 지정합니다.

foo <- function(x) x+1
y <- numeric(1e6)

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 

2020 년 1 월 1 일 업데이트 :

system.time({z1 <- numeric(1e6); for(i in seq_along(y)) z1[i] <- foo(y[i])})
#   user  system elapsed 
#   0.52    0.00    0.53 
system.time(z <- lapply(y, foo))
#   user  system elapsed 
#   0.72    0.00    0.72 
system.time(z3 <- vapply(y, foo, numeric(1)))
#   user  system elapsed 
#    0.7     0.0     0.7 
identical(z1, z3)
# [1] TRUE

원래 결과는 더 이상 사실이 아닙니다. for루프는 Windows 10, 2 코어 컴퓨터에서 더 빠릅니다. 나는 이것을했다5e6 요소들 -루프는 2.9 초 대 3.1 초 vapply였다.

27

다른 곳에서는 Shane과 같은 예제가 실제로 루프에 스트레스를주기보다 함수 내에서 시간이 소비되기 때문에 다양한 종류의 루핑 구문 간의 성능 차이를 실제로 강조하지 않습니다. 또한 코드는 메모리가없는 for 루프를 값을 반환하는 apply family 함수와 부당하게 비교합니다. 요점을 강조하는 약간 다른 예가 있습니다.

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의 간단한 unlist는 0.2 초에 불과하므로 lapply가 훨씬 빠릅니다. for 루프에서 z를 초기화하는 것은 매우 빠릅니다. 마지막 6/6 회 실행의 평균을 제공하므로 시스템 외부로 이동합니다. 거의 영향을 미치지 않습니다)

그러나 주목해야 할 또 하나의 사실은 성능, 선명도 또는 부작용 부족과 관계없이 가족 기능을 적용해야하는 또 다른 이유가 있다는 것입니다. for루프는 일반적으로 루프 내에서 가능한 한 많이두고 추진하고 있습니다. 각 루프에는 다른 가능한 작업 중 정보를 저장하기위한 변수 설정이 필요하기 때문입니다. Apply 문은 다른 방식으로 편향되는 경향이 있습니다. 데이터에 대해 여러 작업을 수행하려는 경우가 종종 있는데, 그 중 일부는 벡터화 할 수 있지만 일부는 불가능할 수도 있습니다. R에서는 다른 언어와 달리 해당 연산을 분리하여 apply 문 (또는 벡터화 된 버전의 함수)에서 벡터화되지 않은 것과 실제 벡터 연산으로 벡터화 된 것을 실행하는 것이 가장 좋습니다. 이것은 종종 성능을 크게 향상시킵니다.

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때문이 아니라 루프의 각 반복에서 하나의 작업 만 수행하기 때문입니다. 이 코드에서는 다른 모든 것이 벡터화됩니다. Joris Meys 전통적인 for루프에서는 각 반복마다 많은 (7?) 연산이 발생하며 실행하기위한 설정이 약간 있습니다. 이것은 for버전 보다 훨씬 더 컴팩트하다는 점도 참고하십시오 .


4
그러나 Shane의 예제는 대부분의 시간 일반적으로 루프가 아닌 함수에 소비 된다는 점에서 현실적입니다 .
hadley

9
직접 말하십시오 ... :) ... 아마도 Shane이 어떤 의미에서는 현실적 일 수도 있지만 같은 의미에서 분석은 전혀 쓸모가 없습니다. 사람들은 많은 반복을 수행해야 할 때 반복 메커니즘의 속도에 관심을 갖습니다. 그렇지 않으면 그들의 문제는 어딘가에 있습니다. 모든 기능에 해당됩니다. 내가 0.001s 걸리는 죄를 쓰고 다른 누군가 0.002 걸리는 죄를 쓰면 ?? 글쎄, 당신이 그들 중 많은 일을해야하자마자 당신이 관심.
John

2
12 코어 3GHz의 인텔 제온 64 비트에, 나는 당신에게 매우 다른 번호를 얻을 - 루프에 대한 상당히 향상 : 내가 얻을, 당신의 세 가지 테스트를 위해 2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528, 그리고 vapply 더 나은입니다 :1.19 0.00 1.19
naught101

2
그것은 OS와 R 버전에 따라 다르며 절대적으로 CPU에 달려 있습니다. 난 그냥 Mac에서 2.15.2로 달려있어 sapply보다 50 % 느리게 for하고 lapply배 빠른.
John

1
귀하의 예에서 (0의 벡터)가 아닌 로 설정 y하는 것을 의미합니다 . 할당하려고 에 잘 일반적인 설명하지 않는 이상 이상과 루프 사용을. 그렇지 않으면 메시지가 표시됩니다. 1:1e6numeric(1e6)foo(0)z[0]for
flodel

3

벡터의 부분 집합에 함수를 적용 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

그러나 이러한 상황에 대해 우리가 가지고 colSumsrowSums:

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

7
(작은 코드 조각의 microbenchmark경우)보다 훨씬 정확하다는 점에 유의해야 system.time합니다. 당신은 비교하려는 경우 system.time(f3(mat))system.time(f4(mat))는 다른 결과를 거의 때마다 얻을 것이다. 때로는 적절한 벤치 마크 테스트만으로 가장 빠른 기능을 보여줄 수 있습니다.
Michele
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.