R開発:サイクルの秘密

1週間前、Hacker誌で、Rで開発する際にサイクルを使用する際の機能に焦点を当てた著者のバージョンの資料公開されました。 大量のデータを処理するときにループを正しく記述する方法を学習します。









注:著者のスペルと句読点が保存されました。







多くのプログラミング言語では、ループは反復タスクに使用される基本的な構成要素として機能します。 ただし、Rでは、ループを過度にまたは誤って使用すると、この言語でループを記述する方法が非常に多くあるにもかかわらず、パフォーマンスが著しく低下する可能性があります。







今日は、Rで定期的なサイクルを使用する機能を検討し、同じ名前のパッケージのforeach



関数にも精通しています。これは、この一見基本的なタスクに対する代替アプローチを提供します。 一方では、 foreach



は最高のネイティブ機能を組み合わせており、他方では、コードへの最小限の変更でシーケンシャルコンピューティングからパラレルコンピューティングに簡単に切り替えることができます。







サイクルについて



そもそも、古典的なプログラミング言語からRに切り替える人にとっては不愉快な驚きであることがよくあります。ループを作成したい場合は、少し考えてみてください。 事実、大量のデータを処理する言語では、原則として、サイクルはクエリ、フィルタリング、集計、およびデータ変換の特殊な機能よりも効率が劣ります。 これは、ほとんどの操作がループではなくSQLクエリ言語を使用して実行されるデータベースの例で簡単に思い出すことができます。







このルールの重要性を理解するために、数字を見てみましょう。 2つの列a



b



非常に単純なテーブルがa



とします。 最初は1から100,000に成長し、2番目は100,000から1に減少します。







 testDF <- data.frame(a = 1:100000, b = 100000:1)
      
      





最初の2つの合計である3番目の列を計算する場合、この種のコードを書くことができる初心者のR開発者が驚くでしょう。







 for(row in 1:nrow(testDF)) testDF[row, 3] <- testDF[row, 1] + testDF[row, 2] # !
      
      





私のラップトップでは、計算に39秒dplyr



ますが、 dplyr



パッケージのテーブルを操作する関数を使用すると、0.009秒で同じ結果が得られます。







 testDF <- testDF %>% mutate(c = a + b)
      
      





このような速度の重大な違いの主な理由は、テーブル内のセルを読み書きする時間の損失です。 これらの段階での最適化のおかげで、特別な機能が勝ちます。 ただし、古き良きサイクルを書き留める必要はありません。サイクルがなければ、本格的なプログラムを作成することはまだ不可能です。 Rのループの内容を見てみましょう。







クラシックループ



Rは、ループを記述する基本的な古典的な方法をサポートしています。









repeat



break



ステートメントが明示的に呼び出されるまでループが繰り返されます。







  repeat expr
      
      





for



while



repeat



常にNULLを返すことに注意してください。これは次のループグループとの違いです。







ベースループを適用する



apply



eapply



lapply



mapply



rapply



sapply



tapply



vapply



-1つのアイデアで結ばれたループ関数のかなり大きなリスト。 ループが適用される対象と返される対象が異なります。







マトリックスに適用される基本的なapply



から始めましょう。







 apply(X, MARGIN, FUN, ...)
      
      





最初のパラメーター( X



)で初期マトリックスを示し、2番目のパラメーター( MARGIN



)でマトリックス走査方法(1-行、2-列、(1,2)-行と列)を指定し、3番目のパラメーターはFUN関数を示します。アイテムごとに呼び出されます。 すべての呼び出しの結果は1つのベクトルまたは行列に結合され、 apply



関数は結果の値として返します。







たとえば、3 x 3の行列m



作成します。







 m <- matrix(1:9, nrow = 3, ncol = 3) print(m) [,1] [,2] [,3] [1,] 1 4 7 [2,] 2 5 8 [3,] 3 6 9
      
      





実際にapply



機能を試してみましょう。







 apply(m, MARGIN = 1, FUN = sum) #      [1] 12 15 18 apply(m, MARGIN = 2, FUN = sum) #      [1] 6 15 24
      
      





簡単にするために、既存のsum



関数をapply



に渡しましたが、関数を使用できます-実際、 apply



はループの本格的な実装です。 たとえば、合計を関数で置き換えます。関数は最初に合計を実行し、合計が15の場合、戻り値を100に置き換えます。







 apply(m, MARGIN = 1, #       FUN = function(x) #       apply { s <- sum(x) #   if (s == 15) #    15,     100 s <- 100 (s) } ) [1] 12 100 18
      
      





このファミリのもう1つの一般的な機能はlapply



です。







 lapply(X, FUN, ...)
      
      





最初のパラメーターはリストまたはベクトルであり、2番目は各要素に対して呼び出す必要がある関数です。 sapply



およびvapply



関数はlapply



ラッパーlapply



。 最初のものは、結果をベクトル、行列、または配列にしようとします。 2番目は、戻り値の型チェックを追加します。







sapply



を使用するかなり一般的な方法は、列を操作することです。 たとえば、テーブルがあります







 data <- data.frame(co1_num = 1, col2_num = 2, col3_char = "a", col4_char = "b")
      
      





sapply



テーブルを転送する場合、列(ベクトル)のリストと見なされます。 したがって、 sapply



sapply



に適用し、呼び出された関数としてdata.frame



を指定して、どの列が数値であるかを確認します。







 sapply(data, is.numeric) co1_num col2_num col3_char col4_char TRUE TRUE FALSE FALSE
      
      





数値の列のみを表示します。







 data[,sapply(data, is.numeric)] co1_num col2_num 1 1 2
      
      





適用ベースのループは、各反復の結果で構成されるループの結果を返すという点で、従来のループとは異なります。







for



最初に書いた遅いループを覚えていますか? ほとんどの時間は、各反復で結果がテーブルに書き込まれるという事実に費やされました。 apply



を使用して最適化されたバージョンを書きましょう。







行ごとの処理を選択して、元のテーブルにapply



し、 apply



関数としてベース加算関数sum



を示します。 その結果、 apply



は各行の列の合計が示されるベクトルを返します。 このベクトルを新しい列として元のテーブルに追加し、目的の結果を取得します。







 a_plus_b <- apply(testDF, 1,sum) testDF$c <- a_plus_b
      
      





実行時間を測定すると、0.248秒と表示されます。これは、最初のオプションよりも100倍高速ですが、テーブルを使用した操作の機能よりも10倍遅いです。







foreach



foreach



はRベースの関数ではありません。 対応するパッケージをインストールし、呼び出す前に接続する必要があります。







 install.packages("foreach") #     ( ) library(foreach) #  
      
      





foreach



はサードパーティの機能であるという事実にもかかわらず、今日ではループを記述するための非常に一般的なアプローチです。 foreach



は、世界で最も尊敬されているR企業の1つであるRevolution Analyticsによって開発されましたforeach



は、商用ディストリビューションRを作成しました。2015年にMicrosoftに買収され、現在、その開発はすべてMicrosoft SQL Server Rサービスの一部です。 ただし、 foreach



は、Apache License 2.0の下でライセンスされる通常のオープンソースプロジェクトです。







foreach



の人気の主な理由:









シンプルから始めましょう。 各反復で1から10までの数値の場合、数値に2が乗算されます。すべての反復の結果は、リストの形式で結果変数に書き込まれます。







 result <- foreach(i = 1:10) %do% (i*2)
      
      





結果をリストではなくベクトルにしたい場合は、結果を結合する関数としてc



を指定する必要があります。







 result <- foreach(i = 1:10, .combine = "c") %do% (i*2)
      
      





+



演算子を使用してそれらを組み合わせることで、すべての結果を単純に追加することもできます。その場合、数値110がresult



変数に書き込まれます。







 result <- foreach(i = 1:10, .combine = "+") %do% (i*2)
      
      





同時に、 foreach



でクロール用のいくつかの変数を指定できます。 変数a



を1から10に成長させ、 b



を10から1に減少させます。その後、10個の数値のベクトル11をresult



として取得します。







 result <- foreach(a = 1:10, b = 10:1, .combine = "c") %do% (a+b)
      
      





ループの繰り返しは、単純な値以上のものを返すことができます。 data.frame



を返す関数があるとします:







 customFun <- function(param) { data.frame(param = param, result1 = sample(1:100, 1), result2 = sample(1:100, 1)) }
      
      





この関数を100回呼び出して、結果を1つのdata.frame



rbind



する場合、 rbind



関数を使用してrbind



できます。







 result <- foreach(param = 1:100,.combine = "rbind") %do% customFun(param)
      
      





結果として、結果変数には単一の結果テーブルがあります。







.combine



では、独自の関数を使用することもでき、追加のパラメーターを使用すると、関数が一度に3つ以上のパラメーターを取ることができる場合にパフォーマンスを最適化できます( foreach



ドキュメントには.multicombine



および.maxcombine



説明が含まれてい.maxcombine



)。







foreach



の主な利点の1つは、順次処理から並列処理への移行が容易なことです。 実際、この移行は%do%



%dopar%



に置き換えることで実行されますが、いくつかのニュアンスがあります。







  1. foreach



    呼び出す前に、並列バックエンドを登録しておく必要があります。 Rには、並列バックエンドdoParallel



    doSNOW



    doMC



    一般的な実装がいくつかあり、それぞれに独自の特性がありますが、簡単にするために、最初のものを選択し、それを接続するコードを数行書くことをお勧めします:







     library(doParallel) #     cl <- makeCluster(8) #  «»    registerDoParallel(cl) #  «»
          
          







各サイクルが1秒間だけ待機する8つの反復サイクルを呼び出すと、すべての反復が並行して起動されるため、サイクルが1秒で機能することがわかります。







  system.time({ foreach(i=1:8) %dopar% Sys.sleep(1) # }) user system elapsed 0.008 0.005 1.014
      
      





並列バックエンドを使用した後、次を停止できます。







  stopCluster(cl)
      
      





foreach



前に毎回並列バックエンドを作成して削除する必要はありません。 原則として、プログラムで一度作成され、それで動作するすべての機能で使用されます。







  1. .packages



    パラメーターを使用して、ワーカースレッドに読み込む必要があるパッケージを明示的に指定する必要があります。


たとえば、 readr



パッケージを使用して各反復でファイルを作成します。このパッケージは、 foreach



呼び出す前にメモリにロードされました。 順次ループ( %do%



)の場合、すべてがエラーなしで機能します。







  library(readr) foreach(i=1:8) %do% write_csv(data.frame(id = 1), paste0("file", i, ".csv"))      (`%dopar%`)    : library(readr) foreach(i=1:8) %do% write_csv(data.frame(id = 1), paste0("file", i, ".csv")) Error in write_csv(data.frame(id = 1), paste0("file", i, ".csv")) : task 1 failed - "could not find function "write_csv""
      
      





このエラーは、 readr



パッケージがパラレルストリーム内にロードされていないために発生します。 .packages



パラメーターを使用してこのエラーを修正します。







  foreach(i=1:8, .packages = "readr") %dopar% write_csv(data.frame(id = 1), paste0("file", i, ".csv"))
      
      





  1. パラレルストリームのコンソール出力は画面に表示されません。 これにより、デバッグが非常に困難になる場合があるため、通常、まず複雑なコードをまず並列処理せずに記述し、その後、 %do%



    %dopar%



    置き換えるか、 sink



    関数を使用して各反復の出力をファイルにリダイレクトします。


結論の代わりに





WWW






All Articles