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は、ループを記述する基本的な古典的な方法をサポートしています。
for
は、最も一般的なタイプのループです。 構文は非常にシンプルで、さまざまなプログラミング言語の開発者になじみがあります。 記事の冒頭で既に使用しようとしました。for
は、各要素に対して渡された機能for
実行します。
# 1 10 for(i in 1:10) print(i) # strings strings <- c("", "", "") for(str in strings) print(str)
あまり一般的ではない
while
とrepeat
、他のプログラミング言語でもよく見られます。while
各反復の前に、論理条件がチェックされ、それが満たされる場合、ループが反復され、そうでない場合、ループは終了します。
while(cond) expr
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
の人気の主な理由:
- 構文は
for
似ています-私が言ったように、最も一般的なタイプのループです。 -
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%
に置き換えることで実行されますが、いくつかのニュアンスがあります。
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
前に毎回並列バックエンドを作成して削除する必要はありません。 原則として、プログラムで一度作成され、それで動作するすべての機能で使用されます。
-
.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"))
- パラレルストリームのコンソール出力は画面に表示されません。 これにより、デバッグが非常に困難になる場合があるため、通常、まず複雑なコードをまず並列処理せずに記述し、その後、
%do%
を%dopar%
置き換えるか、sink
関数を使用して各反復の出力をファイルにリダイレクトします。
結論の代わりに
大量のデータを扱う場合、ループが常に最良の選択とは限りません。 特殊な関数を使用してデータを取得、集計、および変換することは、ループよりも常に効率的です。
Rは多くのループオプションを提供します。 古典的な
for
、while
、およびapply
基づく関数のグループからのrepeat
の主な違いは、apply
が値を返すことです。
- 同じ名前の外部パッケージから
foreach
ループを使用すると、ループの記述を簡素化し、反復によって返される値を柔軟に処理できます。また、マルチスレッド処理に接続することにより、ソリューションのパフォーマンスを向上させることもできます。