web-dev-qa-db-ja.com

Rのループ操作を高速化する

Rには大きなパフォーマンスの問題があります。data.frameオブジェクトを反復処理する関数を作成しました。新しい列をdata.frameに追加し、何かを蓄積するだけです。 (簡単な操作)。 data.frameには約85万行あります。私の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)
}

この操作を高速化する方法はありますか?

181
Kay

最大の問題と非効率性の根本は、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.frameで各関数をnrowで1,000から10,000 x 1,000まで実行し、system.timeで時間を測定します

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

結果は

performance

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)
}

今回はnrowの10,000から10,000までのこの関数の結果を比較します。

performance

調整済みのチューニング

もう1つの微調整は、ループ内でtemp[i,9]res[i]に変更することです(i番目のループの繰り返しでまったく同じです)。ここでも、ベクトルのインデックス付けとdata.frameのインデックス付けの違いです。
2番目:ループを見ると、すべての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行の計算時間がかかります。

performance

さらに先に進んでほしいと思っています。少なくとも2つのことができます。

  • 条件付き累積を行うCコードを書く
  • データの最大シーケンスが大きくないことがわかっている場合は、ループをベクトル化されたwhileに変更できます

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

シミュレーションと数値に使用されるコードは GitHubで利用可能 です。

419
Marek

Rコードを高速化するための一般的な戦略

最初に、where遅い部分が実際にあることを理解します。ゆっくり実行されていないコードを最適化する必要はありません。少量のコードの場合、単純にそれを熟考するだけで機能します。それが失敗した場合、RProfおよび同様のプロファイリングツールが役立ちます。

ボトルネックを特定したら、より効率的なアルゴリズムについて考えてみてください。計算は、可能であれば1回だけ実行する必要があります。

より多くの効率的な関数を使用すると、中程度または大きな速度向上が得られます。たとえば、paste0はわずかな効率の向上をもたらしますが、.colSums()とその親類は多少顕著なゲインを実現します。 mean特に遅い です。

そうすれば、特によくあるトラブルを避けることができます:

  • cbindを使用すると、速度が大幅に低下します。
  • データ構造を初期化してから、 毎回展開するのではなく で入力します。
  • 事前割り当てでも、値渡しアプローチではなく参照渡しアプローチに切り替えることができますが、面倒な価値はないかもしれません。
  • 避けるべき落とし穴については、 R Inferno をご覧ください。

より良いvectorizationを試してみてください。これは常に役立つとは限りません。この点で、ifelsediffなどの本質的にベクトル化されたコマンドは、applyコマンドファミリよりも多くの改善を提供します(よく書かれたループで速度をほとんどまたはまったく向上させません)。

R関数にさらに情報を提供するを試みることもできます。たとえば、vapplyではなく sapply を使用し、テキストベースのデータを読み込むときに colClasses を指定します。速度のゲインは、推測の量に応じて変わります。

次に、最適化されたパッケージを検討してください: data.table パッケージは、データ操作や大量のデータの読み取り(fread)での使用が可能な場合に、大幅な速度向上を実現できます。

次に、Rを呼び出すより効率的な手段を使用して速度の向上を試みます。

  • Rスクリプトをコンパイルします。または、Raおよびjitパッケージをジャストインタイムコンパイルに合わせて使用​​します(Dirkには このプレゼンテーション の例があります)。
  • 最適化されたBLASを使用していることを確認してください。これらは全面的な速度の向上を提供します。正直なところ、Rがインストール時に最も効率的なライブラリを自動的に使用しないのは残念です。 Revolution Rがここで行った作業をコミュニティ全体に貢献することを願っています。
  • Radford Nealは多くの最適化を行っており、その一部はR Coreに採用され、他の多くは pqR に分岐しました。

そして最後に、上記のすべてで必要な速度が得られない場合は、遅いコードスニペットの速い言語に移行する必要があります。ここでRcppinlineを組み合わせると、アルゴリズムの最も遅い部分のみをC++コードで置き換えることが特に簡単になります。ここで、たとえば、 初めての試み であり、高度に最適化されたRソリューションでさえも吹き飛ばされます。

それでもやはり問題が解決しない場合は、より多くの計算能力が必要です。 parallelizationhttp://cran.r-project.org/web/views/HighPerformanceComputing.html )またはGPUベースのソリューション(gpu-tools)を調べます。

他のガイダンスへのリンク

133
Ari B. Friedman

forループを使用している場合、RをCまたはJavaまたは他の何かであるかのようにコーディングしている可能性があります。適切にベクトル化されたRコードは非常に高速です。

たとえば、次の2つの簡単なコードを使用して、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 
34
Andrie

これは、インデックスまたはネストされた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."
17
Shane

Ariが答えの最後で述べたように、Rcppおよびinlineパッケージを使用すると、物事を非常に簡単に高速化できます。例として、この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)
}

#includeingの事柄についても同様の手順があり、パラメーターを渡すだけです。

inc <- '#include <header.h>

include=incとしてcxxfunctionに。これについて本当に素晴らしいのは、それがあなたのためにすべてのリンクとコンパイルを行うということです。そのため、プロトタイピングは非常に高速です。

免責事項:tmpのクラスが数値であり、数値行列などではないことを完全に確信していません。しかし、私はほとんど確信しています。

編集:この後さらに速度が必要な場合、 OpenMPC++に適した並列化機能です。私はinlineからそれを使用しようとしませんでしたが、それは動作するはずです。 nコアの場合、ループの反復kk % nで実行するという考え方です。適切な紹介はMatloffのThe Art of R Programmingにあります here 、16章、Resorting to C

7
jclancy

コードの書き換えは嫌いです...もちろん、ifelseとlapplyの方が優れたオプションですが、それを適合させるのが難しい場合もあります。

df$var[i]などのリストを使用するように、頻繁にdata.framesを使用します

次に例を示します。

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)
7
Chris

ここでの答えは素晴らしいです。カバーされていないマイナーな側面の1つは、質問に「私の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)
}
3
rookie

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]

ベクトル化された算術演算はより多くの時間と問題について考える必要がありますが、実行時間を数桁節約できる場合があります。

2
Calimo

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

条件フィルタリングから得られる可能性のあるゲインを無視すると、非常に高速になります。明らかに、データのサブセットに対して計算を行うことができれば、役立ちます。

0
Bulat