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

この操作をスピードアップする方法はありますか?

回答:


433

最大の問題と非効率の根源は、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、名前をいじる必要はありません。それで、それはどれほど良いのですか?

1,000から10,000まで1,000で各関数を実行data.framenrow、時間を測定します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秒かかると予測されています。

ベクトル化の力

シェーンとカリモが回答に述べているように、ベクトル化はパフォーマンス向上の鍵です。あなたのコードからループの外に移動できます:

  • コンディショニング
  • 結果の初期化(ある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から100,000で10,000です。

パフォーマンス

tunedのチューニング

もう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行の計算時間がかかります。

パフォーマンス

私はあなたがあなたがさらに先に進むことができることを望んでいます、私は行うことができる少なくとも2つのことを見ます:

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

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

シミュレーションと図に使用されるコードはGitHub入手できます


2
マレクに個人的に尋ねる方法を見つけることができないので、それらのグラフはどのように生成されましたか?
Carbontwelve 2013

@carbontwelveデータやプロットについて質問していますか?プロットはラティスパッケージで作成されました。時間があれば、コードをWebのどこかに配置して通知します。
Marek 2013

@carbontwelveおっと、私は間違っていました:)これは(ベースRからの)標準プロットです。
Marek 2013

@Gregor残念ながらできません。累積的であるため、ベクトル化できません。簡単な例:res = c(1,2,3,4)and condis allのTRUE場合、最終結果は次のようになります。13(原因1+2)、 6(原因の第二は今3、第三は3また)、 ()。10 6+4あなたが得たシンプルな和をやって1357
Marek 2016年

ああ、もっと注意深く考えるべきだった。間違いを見せてくれてありがとう。
Gregor Thomas

132

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

まず、どこを見つけます遅い部分が実際にあるかをます。実行速度が遅いコードを最適化する必要はありません。少量のコードの場合、単純にそれを考えるだけでうまくいきます。それが失敗した場合は、RProfおよび同様のプロファイリングツールが役立ちます。

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

より効率的な関数を使用すると、中程度または大きな速度が向上します。たとえばpaste0、小さな効率の向上が得られますが.colSums()、その親類はやや顕著なゲインを生み出します。 meanある特に遅いです

次に、いくつかの特に一般的なトラブルを回避できます。

  • cbind 本当にあなたを遅くします。
  • データ構造を初期化し、毎回拡張するのではなく、データ構造を入力します。
  • 事前割り当てを使用しても、値渡しアプローチではなく参照渡しアプローチに切り替えることができますが、面倒な価値はありません。
  • 避けるべき落とし穴については、Rインフェルノをご覧ください。

ベクトル化の改善を試みてください。この点で、本質的にベクトル化コマンドが好きifelsediffなどがより多くの改善を提供しますapplyファミリます(適切に記述されたループに対して速度がほとんどまたはまったく向上しません)。

あなたも試すことができます R関数に詳細情報を提供するます。たとえば、ではvapplyなくをsapply使用しcolClasses、テキストベースのデータを読み取るときに指定します。速度の向上は、推測をどれだけ排除するかによって異なります。

次に、最適化されたパッケージを検討します。このdata.tableパッケージは、データ操作や大量のデータの読み取りなど、その使用が可能な場所で大幅な速度向上を生み出します(fread)ます。

次に、Rを呼び出すより効率的な方法を通じて速度の向上を試みます。

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

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

それでも問題が解決しない場合は、より多くの計算能力が必要です。見て並列化 http://cran.r-project.org/web/views/HighPerformanceComputing.html)あるいはGPUベースのソリューション(gpu-tools)。

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


35

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 

+1あなたの2番目の例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)})、同様の結果になります。
ヘンリー

@ヘンリー、公平なコメント、しかしあなたが指摘するように、結果は同じです。例を変更してaを初期化しました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つの行がありません。
ケイ

データサンプルを提供する場合(いくつかの行でdput()を使用)、それを修正します。which()-1ビットのため、インデックスは等しくありません。しかし、ここからそれがどのように機能するかがわかるはずです。ループや適用の必要はありません。ベクトル化された関数を使用するだけです。
シェーン

1
うわー!ネストされたif..else関数ブロックとmapplyをネストされたifelse関数に変更し、200倍の高速化を実現しました。
James

あなたの一般的なアドバイスは正しいですが、コードで事実を見逃していることですが、そのi-thの値はi-1-thに依存しているため、(を使用して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)、andの代わりにこれを実行してください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に関して興味深い質問をするでしょう。
フランク

2
@Frank(i)変更されたオブジェクトが有効なdata.frameであることを確認する必要があり、(ii)afaikは少なくとも1つのコピーを作成します。データフレームのサブ割り当ては遅いことが知られており、長いソースコードを見ると、それほど驚くことではありません。
ローランド

@フランク、@ローランド:df$var[i]表記は同じ[<-.data.frame関数を通過しますか?本当に長いことに気づきました。そうでない場合、どの機能を使用しますか?
クリス

@Chris d$foo[i]=markはだいたいに翻訳されると思いますがd <- `$<-`(d, 'foo', `[<-`(d$foo, i, mark))、一時的な変数をいくつか使用しています。
Tim Goodman、

7

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

#includeパラメータを渡すだけで、物事を処理するための同様の手順があります

inc <- '#include <header.h>

としてcxxfunctionに include=inc。これの本当にすばらしいところは、すべてのリンクとコンパイルが自動的に行われるため、プロトタイピングが非常に高速であることです。

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

編集:この後も速度を上げる必要がある場合、OpenMPはに適した並列化機能ですC++。からの使用は試していませんinlineが、問題なく動作するはずです。アイデアは、nコアの場合、ループ反復をkによって実行させることk % nです。適切な導入は、ここで利用可能なMatloffのThe Art of R Programmingの第16章「Resorting to C」にあります。


3

ここでの答えは素晴らしいです。カバーされていないマイナーな側面の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)
}

重ね合わせでも動作します。

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)をループしているので、いつものようなものを持っています。
フランク

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ロング

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.