R에서 루프 작동 속도 향상


193

R에서 큰 성능 문제가 data.frame있습니다. 객체 를 반복하는 함수를 작성했습니다 . 단순히 새로운 열을 추가하고 data.frame무언가를 축적합니다. (간단한 조작). 는 data.frame대략 850K 행이 있습니다. 내 PC는 여전히 작동하고 있으며 (현재 약 10 시간) 런타임에 대해서는 전혀 모릅니다.

dayloop2 <- function(temp){
    for (i in 1:nrow(temp)){    
        temp[i,10] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                temp[i,10] <- temp[i,9] + temp[i-1,10]                    
            } else {
                temp[i,10] <- temp[i,9]                                    
            }
        } else {
            temp[i,10] <- temp[i,9]
        }
    }
    names(temp)[names(temp) == "V10"] <- "Kumm."
    return(temp)
}

이 작업 속도를 높이는 방법에 대한 아이디어가 있습니까?

답변:


435

가장 큰 문제와 비효율의 뿌리는 data.frame을 인덱싱하는 것 temp[,]입니다.
이것을 최대한 피하십시오. 나는 당신의 기능을 가지고, 색인 생성을 변경하고 여기 version_A

dayloop2_A <- function(temp){
    res <- numeric(nrow(temp))
    for (i in 1:nrow(temp)){    
        res[i] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                res[i] <- temp[i,9] + res[i-1]                   
            } else {
                res[i] <- temp[i,9]                                    
            }
        } else {
            res[i] <- temp[i,9]
        }
    }
    temp$`Kumm.` <- res
    return(temp)
}

보시다시피 res결과를 수집하는 벡터 를 만듭니다 . 결국 나는 그것을 추가하고 data.frame이름을 엉망으로 만들 필요가 없습니다. 그래서 얼마나 낫습니까?

나는 각 기능을 실행 data.framenrow1,000에서 1,000 10,000과 함께 시간을 측정system.time

X <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
system.time(dayloop2(X))

결과는

공연

버전이에서 지수에 의존한다는 것을 알 수 있습니다 nrow(X). 수정 된 버전에는 선형 관계가 있으며 간단한 lm모델에서는 850,000 개의 행 계산에 6 분 10 초가 소요될 것으로 예측합니다.

벡터화의 힘

Shane과 Calimo의 답변에 따르면 벡터화는 성능 향상의 핵심입니다. 코드에서 루프 외부로 이동할 수 있습니다.

  • 조절
  • 결과의 초기화 ( temp[i,9])

이 코드로 연결됩니다

dayloop2_B <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in 1:nrow(temp)) {
        if (cond[i]) res[i] <- temp[i,9] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

이 함수의 결과를 nrow10,000 ~ 100,000 x 10,000으로 비교합니다.

공연

튜닝 튜닝

또 다른 조정은 루프 인덱싱 temp[i,9]에서 res[i](i 번째 루프 반복에서와 동일하게) 변경하는 것입니다. 벡터 인덱싱과 a 인덱싱의 차이점이 다시 data.frame있습니다.
두 번째로 : 루프를 볼 때 모든 루프를 반복 할 필요는 없지만 i조건에 맞는 루프 만 필요하다는 것을 알 수 있습니다.
그래서 우리는 간다

dayloop2_D <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in (1:nrow(temp))[cond]) {
        res[i] <- res[i] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

얻는 성능은 데이터 구조에 따라 다릅니다. 정확하게- TRUE조건 값의 백분율 . 시뮬레이션 된 데이터의 경우 1 초 미만의 850,000 행에 계산 시간이 걸립니다.

공연

나는 당신이 더 나아갈 수 있기를 원합니다. 나는 할 수있는 적어도 두 가지를 봅니다.

  • C조건부 적산을 수행 하는 코드 작성
  • 데이터 최대 시퀀스가 ​​크지 않다는 것을 알고 있다면 루프를 벡터화로 변경할 수 있습니다.

    while (any(cond)) {
        indx <- c(FALSE, cond[-1] & !cond[-n])
        res[indx] <- res[indx] + res[which(indx)-1]
        cond[indx] <- FALSE
    }

시뮬레이션 및 수치에 사용되는 코드는 GitHub에서 사용할 수 있습니다 .


2
Marek에게 사소한 질문을하는 방법을 찾을 수 없어서 어떻게 그래프가 생성 되었습니까?
carbontwelve

@carbontwelve 데이터 나 플롯에 대해 질문하고 있습니까? 격자 패키지로 플롯을 만들었다. 시간이 있으면 웹 어딘가에 코드를 넣고 통지합니다.
Marek

@ carbontwelve Ooops, 내가 틀렸다 :) 이것은 표준 플롯입니다 (베이스 R에서).
Marek

@Gregor 불행히도 아닙니다. 누적되므로 벡터화 할 수 없습니다. 간단한 예 : res = c(1,2,3,4)cond모든 TRUE다음, 최종 결과가 같아야 1, 3(원인 1+2), 6(원인 제 지금 3, 및 세 번째는 3도) 10( 6+4). 당신이있어 간단한 요약을하는 1, 3, 5, 7.
Marek

아, 좀 더 신중하게 생각해야 했어요. 실수를 보여 주셔서 감사합니다.
Gregor Thomas

132

R 코드 속도를 높이기위한 일반적인 전략

먼저 느린 부분이 실제로 어디에 있는지 파악 하십시오. 느리게 실행되지 않는 코드를 최적화 할 필요가 없습니다. 소량의 코드의 경우 단순히 코드를 통해 생각하면 효과가 있습니다. 실패하면 RProf 및 유사한 프로파일 링 도구가 도움이 될 수 있습니다.

병목 현상을 파악한 후 원하는 작업을 수행 하기 위한 보다 효율적인 알고리즘 에 대해 생각해보십시오 . 계산은 가능하면 한 번만 실행해야합니다.

보다 효율적인 기능을 사용하면 중간 또는 큰 속도 이득을 얻을 수 있습니다. 예를 들어, paste0작은 효율 이득을 생성하지만 .colSums()그 친족들은 다소 더 뚜렷한 이득을 생성합니다. mean이다 특히 느린 .

그런 다음 특히 일반적인 문제를 피할 수 있습니다 .

  • cbind 정말 빨리 속도를 늦출 것입니다.
  • 데이터 구조를 초기화 한 다음 매번 확장하는 대신 채우십시오 .
  • 사전 할당을 사용하더라도 값별 접근 방식 대신 참조 별 접근 방식으로 전환 할 수 있지만 번거롭지 않을 수 있습니다.
  • 더 많은 함정을 피하기 위해 R Inferno 를 살펴보십시오 .

항상 도움이 될 수는 있지만 더 나은 벡터화를 시도하십시오 . 이와 관련, 본질적으로 벡터화 명령 좋아 ifelse, diff그리고 같은이보다 더 향상을 제공 할 것입니다 apply(잘 작성 루프를 통해 더 속도 향상에 조금 제공) 명령의 가족.

R 함수에 더 많은 정보를 제공 할 수도 있습니다 . 예를 들어, vapply대신을sapply 사용 colClasses하고 텍스트 기반 데이터를 읽을 때 지정하십시오 . 얼마나 많은 추측을 제거 하느냐에 따라 속도 게인이 달라집니다.

다음으로 최적화 된 패키지를 고려 하십시오 . data.table패키지는 데이터 조작 및 대량의 데이터 읽기 ( fread) 에서 사용이 가능한 경우 엄청난 속도 향상을 가져올 수 있습니다 .

다음으로 보다 효율적인 R 호출 방법을 통해 속도 향상을 시도하십시오 .

  • R 스크립트를 컴파일하십시오. 또는 적시에 컴파일 하기 위해 함께 Rajit패키지를 함께 사용하십시오 (Dirk는 이 프리젠 테이션에 예가 있습니다 ).
  • 최적화 된 BLAS를 사용하고 있는지 확인하십시오. 이는 전반적으로 속도 향상을 제공합니다. 솔직히 R이 설치시 가장 효율적인 라이브러리를 자동으로 사용하지 않는 것은 부끄러운 일입니다. Revolution R이 그들이 한 작업을 전체 커뮤니티에 기여할 수 있기를 바랍니다.
  • Radford Neal은 여러 가지 최적화를 수행했으며 그 중 일부는 R Core에 채택되었고 다른 일부는 pqR 로 분기되었습니다 .

마지막으로 위의 모든 사항이 여전히 필요한만큼 빠르지 않으면 느린 코드 스 니펫을 위해 더 빠른 언어 로 이동해야 할 수도 있습니다 . Rcpp그리고 inline여기 의 조합은 알고리즘의 가장 느린 부분 만 C ++ 코드로 쉽게 대체 할 수있게합니다. 예를 들어 여기에 대한 첫 번째 시도가 있으며 , 고도로 최적화 된 R 솔루션도 날려 버립니다.

이 모든 후에도 여전히 문제가 발생하면 더 많은 컴퓨팅 능력이 필요합니다. 병렬화 ( http://cran.r-project.org/web/views/HighPerformanceComputing.html ) 또는 GPU 기반 솔루션 ( gpu-tools)을 살펴보십시오 .

다른 지침에 대한 링크


36

for루프 를 사용 하는 경우 R이 C 또는 Java 인 것처럼 R을 코딩 할 가능성이 높습니다. 제대로 벡터화 된 R 코드는 매우 빠릅니다.

예를 들어 다음 두 간단한 비트 코드를 사용하여 10,000 개의 정수 목록을 순서대로 생성하십시오.

첫 번째 코드 예제는 전통적인 코딩 패러다임을 사용하여 루프를 코딩하는 방법입니다. 완료하는 데 28 초가 걸립니다

system.time({
    a <- NULL
    for(i in 1:1e5)a[i] <- i
})
   user  system elapsed 
  28.36    0.07   28.61 

사전 할당 메모리의 간단한 동작으로 거의 100 배나 개선 할 수 있습니다.

system.time({
    a <- rep(1, 1e5)
    for(i in 1:1e5)a[i] <- i
})

   user  system elapsed 
   0.30    0.00    0.29 

그러나 콜론 연산자를 사용하여 기본 R 벡터 연산을 사용하면 :이 연산은 사실상 즉각적입니다.

system.time(a <- 1:1e5)

   user  system elapsed 
      0       0       0 

+1하지만 두 번째 예는 a[i]변하지 않는 것처럼 설득력이없는 것으로 간주합니다 . 그러나 system.time({a <- NULL; for(i in 1:1e5){a[i] <- 2*i} }); system.time({a <- 1:1e5; for(i in 1:1e5){a[i] <- 2*i} }); system.time({a <- NULL; a <- 2*(1:1e5)})비슷한 결과가 있습니다.
헨리

@ 헨리, 공정한 의견이지만, 지적한 것처럼 결과는 동일합니다. rep(1, 1e5)타이밍을 동일하게 초기화하는 예제를 수정했습니다 .
Andrie

17

인덱스 나 중첩 된 ifelse()명령문 을 사용하여 루프를 건너 뛰면 훨씬 빨라질 수 있습니다 .

idx <- 1:nrow(temp)
temp[,10] <- idx
idx1 <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
temp[idx1,10] <- temp[idx1,9] + temp[which(idx1)-1,10] 
temp[!idx1,10] <- temp[!idx1,9]    
temp[1,10] <- temp[1,9]
names(temp)[names(temp) == "V10"] <- "Kumm."

답변 해주셔서 감사합니다. 나는 당신의 진술을 이해하려고 노력합니다. 4 행 : "temp [idx1,10] <-temp [idx1,9] + temp [which (idx1) -1,10]"는 더 긴 객체의 길이가 길이의 배수가 아니기 때문에 오류를 일으켰습니다. 더 짧은 물체. "temp [idx1,9] = num [1 : 11496]"및 "temp [which (idx1) -1,10] = int [1 : 11494]"이므로 2 개의 행이 없습니다.
Kay

데이터 샘플을 제공하면 (몇 줄로 dput () 사용) 수정 해 드리겠습니다. which ()-1 비트로 인해 인덱스가 동일하지 않습니다. 그러나 여기에서 어떻게 작동하는지 확인해야합니다. 루핑하거나 적용 할 필요가 없습니다. 벡터화 된 함수 만 사용하십시오.
셰인

1
와! 방금 중첩 된 if..else 함수 블록을 변경하고 중첩 된 ifelse 함수로 매핑하고 200 배 속도 향상했습니다!
James

일반적인 조언은 옳지 만 코드에서 i-값은-값에 의존 i-1하므로 사용자가 수행하는 방식으로 설정할 수 없다는 사실을 놓쳤 습니다 ( which()-1).
Marek

8

코드 재 작성을 싫어합니다 ... 물론 ifelse와 lapply가 더 나은 옵션이지만 때로는 적합하게 만들기가 어렵습니다.

종종 다음과 같은 목록을 사용하는 것처럼 data.frames를 사용합니다. df$var[i]

다음은 구성 예입니다.

nrow=function(x){ ##required as I use nrow at times.
  if(class(x)=='list') {
    length(x[[names(x)[1]]])
  }else{
    base::nrow(x)
  }
}

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
})

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  d=as.list(d) #become a list
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
  d=as.data.frame(d) #revert back to data.frame
})

data.frame 버전 :

   user  system elapsed 
   0.53    0.00    0.53

리스트 버전 :

   user  system elapsed 
   0.04    0.00    0.03 

data.frame보다 벡터 목록을 사용하면 17 배 더 빠릅니다.

내부적으로 data.frames가 왜 그렇게 느린 지에 대한 의견이 있습니까? 하나는 그들이 목록처럼 작동한다고 생각할 것입니다 ...

더 빠른 코드에 대해 이렇게 class(d)='list'대신 d=as.list(d)하고class(d)='data.frame'

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  class(d)='list'
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
  class(d)='data.frame'
})
head(d)

1
아마도의 오버 헤드 덕분에 아마도 수정 [<-.data.frame될 때마다 호출 d$foo[i] = mark되어 전체 data.frame 벡터의 새 복사본을 만들 수 있습니다 <-. SO에 대한 흥미로운 질문을 할 것입니다.
Frank

2
@ 프랭크 그것은 (i) 수정 된 객체가 여전히 유효한 data.frame인지 확인해야하며 (ii) afaik은 적어도 하나 이상의 사본을 만들 수 있습니다. 데이터 프레임 하위 할당은 느리다고 알려져 있으며 긴 소스 코드를 보면 그리 놀라운 일이 아닙니다.
Roland

@Frank, @Roland : df$var[i]표기법이 동일한 [<-.data.frame기능을 수행합니까? 나는 그것이 정말로 길다는 것을 알았다. 그렇지 않은 경우 어떤 기능을 사용합니까?
Chris

@Chris 나는 d$foo[i]=mark대략적 d <- `$<-`(d, 'foo', `[<-`(d$foo, i, mark))으로 변수 로 번역 되지만 임시 변수를 사용 한다고 생각 합니다.
Tim Goodman

7

Ari가 그의 대답 끝에 언급했듯이 Rcppinline패키지를 사용하면 작업을 매우 빠르게 수행 할 수 있습니다. 예를 들어이 inline코드를 사용해보십시오 (경고 : 테스트되지 않음).

body <- 'Rcpp::NumericMatrix nm(temp);
         int nrtemp = Rccp::as<int>(nrt);
         for (int i = 0; i < nrtemp; ++i) {
             temp(i, 9) = i
             if (i > 1) {
                 if ((temp(i, 5) == temp(i - 1, 5) && temp(i, 2) == temp(i - 1, 2) {
                     temp(i, 9) = temp(i, 8) + temp(i - 1, 9)
                 } else {
                     temp(i, 9) = temp(i, 8)
                 }
             } else {
                 temp(i, 9) = temp(i, 8)
             }
         return Rcpp::wrap(nm);
        '

settings <- getPlugin("Rcpp")
# settings$env$PKG_CXXFLAGS <- paste("-I", getwd(), sep="") if you want to inc files in wd
dayloop <- cxxfunction(signature(nrt="numeric", temp="numeric"), body-body,
    plugin="Rcpp", settings=settings, cppargs="-I/usr/include")

dayloop2 <- function(temp) {
    # extract a numeric matrix from temp, put it in tmp
    nc <- ncol(temp)
    nm <- dayloop(nc, temp)
    names(temp)[names(temp) == "V10"] <- "Kumm."
    return(temp)
}

#include매개 변수를 전달하는 비슷한 절차 가 있습니다.

inc <- '#include <header.h>

cxxfunction에 include=inc . 정말 멋진 점은 모든 링크 및 컴파일 작업을 수행하므로 프로토 타이핑이 정말 빠르다는 것입니다.

면책 조항 : tmp 클래스가 숫자 행렬이어야하며 숫자 행렬이나 다른 것이 아니라는 것을 확신하지 못합니다. 그러나 나는 대부분 확신합니다.

편집 :이 후에도 속도가 더 필요한 경우 OpenMP 는 병렬 처리 기능입니다 C++. 에서 사용해 보지 않았지만 inline작동해야합니다. n코어 의 경우에 k의해 루프 반복 을 수행하는 것이 k % n좋습니다. Matloff에서 발견되는 적절한 도입의 R 프로그래밍의 예술 을 사용할 수, 여기 , 장 16, C에 의존 .


3

여기에 대한 답변이 훌륭합니다. 다루지 않은 한 가지 사소한 측면은 " 내 PC가 여전히 작동하고 있으며 (약 10 시간) 실행에 대해 전혀 모른다 "는 질문입니다. 변경 사항이 속도에 어떻게 영향을 미치는지 및 완료하는 데 걸리는 시간을 모니터링하기 위해 개발할 때 항상 다음 코드를 루프에 넣습니다.

dayloop2 <- function(temp){
  for (i in 1:nrow(temp)){
    cat(round(i/nrow(temp)*100,2),"%    \r") # prints the percentage complete in realtime.
    # do stuff
  }
  return(blah)
}

lapply 와도 작동합니다.

dayloop2 <- function(temp){
  temp <- lapply(1:nrow(temp), function(i) {
    cat(round(i/nrow(temp)*100,2),"%    \r")
    #do stuff
  })
  return(temp)
}

루프 내의 기능이 상당히 빠르지 만 루프 수가 많으면 콘솔 자체에 인쇄하는 데 오버 헤드가 있으므로 자주 인쇄하는 것이 좋습니다. 예 :

dayloop2 <- function(temp){
  for (i in 1:nrow(temp)){
    if(i %% 100 == 0) cat(round(i/nrow(temp)*100,2),"%    \r") # prints every 100 times through the loop
    # do stuff
  }
  return(temp)
}

비슷한 옵션으로 분수 i / n을 인쇄하십시오. 나는 cat(sprintf("\nNow running... %40s, %s/%s \n", nm[i], i, n))보통 이름이 붙은 것들 (이름이 인 nm)을 반복하기 때문에 항상 비슷한 것을 가지고 있습니다 .
Frank

2

R에서는 apply패밀리 기능 을 사용하여 루프 처리 속도를 높이는 경우가 있습니다 (경우에 따라 다름 replicate). 좀 봐plyr진행률 표시 줄을 제공 패키지를 .

또 다른 옵션은 루프를 완전히 피하고 벡터화 된 산술로 대체하는 것입니다. 정확히 무엇을하고 있는지 확실하지 않지만 한 번에 모든 행에 함수를 적용 할 수 있습니다.

temp[1:nrow(temp), 10] <- temp[1:nrow(temp), 9] + temp[0:(nrow(temp)-1), 10]

이것은 훨씬 빠르며 조건에 따라 행을 필터링 할 수 있습니다.

cond.i <- (temp[i, 6] == temp[i-1, 6]) & (temp[i, 3] == temp[i-1, 3])
temp[cond.i, 10] <- temp[cond.i, 9]

벡터화 된 산술에는 문제에 대해 더 많은 시간과 생각이 필요하지만 때로는 실행 시간을 몇 배나 줄일 수 있습니다.


14
벡터 함수가 루프 또는 apply ()보다 빠르다는 점에 주목하지만 apply ()가 루프보다 빠르다는 것은 사실이 아닙니다. 많은 경우 apply ()는 단순히 루프를 사용자로부터 멀어 지도록 추상화하지만 여전히 루핑합니다. 이 이전 질문을 참조하십시오 : stackoverflow.com/questions/2275896/…
JD Long

0

처리 방법은 다음과 data.table같습니다.

n <- 1000000
df <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
colnames(df) <- paste("col", 1:9, sep = "")

library(data.table)

dayloop2.dt <- function(df) {
  dt <- data.table(df)
  dt[, Kumm. := {
    res <- .I;
    ifelse (res > 1,             
      ifelse ((col6 == shift(col6, fill = 0)) & (col3 == shift(col3, fill = 0)) , 
        res <- col9 + shift(res)                   
      , # else
        res <- col9                                 
      )
     , # else
      res <- col9
    )
  }
  ,]
  res <- data.frame(dt)
  return (res)
}

res <- dayloop2.dt(df)

m <- microbenchmark(dayloop2.dt(df), times = 100)
#Unit: milliseconds
#       expr      min        lq     mean   median       uq      max neval
#dayloop2.dt(df) 436.4467 441.02076 578.7126 503.9874 575.9534 966.1042    10

조건 필터링에서 얻을 수있는 이득을 무시하면 매우 빠릅니다. 데이터 하위 집합에서 계산을 수행 할 수 있으면 도움이됩니다.


2
data.table을 사용하라는 제안을 반복하는 이유는 무엇입니까? 이전 답변에서 이미 여러 번 작성되었습니다.
IRTFM
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.