apply ファミリーを一文で
apply ファミリーのすべてのメンバーは同じ仕事をします。関数を受け取り、ある構造のすべての要素に対して実行し、結果を集める ことです。異なるのは入力の形と出力の形です。対応表は次のとおりです。
apply(m, MARGIN, FUN)— 行列 の行または列。lapply(x, FUN)— リストやベクトルの各要素。常にリストを返します。sapply(x, FUN)—lapplyと同じで、可能ならベクトルや行列に簡略化します。vapply(x, FUN, FUN.VALUE)— 戻り値の型を宣言して強制するsapply。mapply(FUN, x, y, ...)— 複数の入力を要素ごとに並行してたどります。tapply(values, groups, FUN)— 各グループ内でFUNを適用し、グループごとに 1 つの結果を返します。
いずれも、無名関数を含めて自分で書いた 関数 を受け取れます。ここにこのファミリーの力の源があります。
apply(): 行列の行と列
apply は行列、MARGIN(行なら 1、列なら 2)、そして関数を受け取ります。
行列は列方向に埋まるので、行は 1 3 5 と 2 4 6 になります。行の合計は 9 12、列の合計は 3 7 11 と表示されます。最もよくある 2 つのケースについては、base R が専用のより高速なヘルパー — rowSums, colSums, rowMeans, colMeans — を用意しているので、apply は apply(m, 2, max) のように専用関数がない場合に取っておきましょう。
1 つ罠があります。データフレームに apply を使うと、内部で黙って行列に変換され、すべての列が共通の 1 つの型に強制されます。データフレームでは列をリストとして扱い、代わりに lapply / sapply を使いましょう。
lapply() は常にリスト、sapply() は簡略化する
lapply は各要素に関数を適用し、何があっても同じ長さの リスト を返します。
計算は同じでも形は 2 通りです。lapply は 85 と 80 を保持するリストを返し、sapply はすべての結果の長さが 1 だと気づいて名前付き数値ベクトルに簡略化します。読むにも、さらなる計算に渡すにもはるかに快適です。
ただしこの利便性には鋭い刃があります。sapply の戻り値の型はデータに依存する のです。1 つの要素でも異なる長さを生むと簡略化は失敗し、黙ってリストが返ってきます。
対話的な作業なら肩をすくめて済む話ですが、スクリプトの中では、1 年間動いていたコードがデータの形が変わった日に壊れることを意味します。vapply が存在する理由はまさにこれです。
vapply(): 型安全な sapply
vapply は 3 番目の引数 FUN.VALUE を追加します。1 つの結果がどうあるべきかを宣言するテンプレートです。integer(1) は「各呼び出しはちょうど 1 個の整数を返す」という意味です。
名前付き整数ベクトル 5 6 6 が得られ、さらに重要なことに保証が得られます。もし nchar が 2 つの値や文字列を返すようになったら、vapply は形の崩れた結果を下流へ流す代わりに、その場でエラーを出して止まります。この宣言はドキュメントの役割も果たします。
vapply(words, nchar, character(1))
# Error in vapply(words, nchar, character(1)) : values must be type 'character'
経験則: コンソールでは sapply、無人で走らなければならない関数やスクリプトでは vapply。
mapply() と tapply(): 並行入力とグループ
lapply は 1 つの構造をたどります。各呼び出しが 複数の 構造の対応する位置から要素を必要とする場合は mapply を使います。関数が最初に来ることに注意してください。
出力の各要素は、1 番目どうし、2 番目どうし、というように値を組み合わせます: 10 200 3000 40000。
tapply はグループ単位のメンバーです。values を groups で分割し、各グループ内で関数を適用して、グループごとに 1 つの結果を返します。
グループ a の平均は 30、グループ b の平均は 40 です。これは「カテゴリごとの平均」に対する base R の答えであり、データフレームに対して group_by + summarize が答えるのと同じ形の問いです。すでに dplyr の領域にいるならそちらを使いましょう。tapply は裸のベクトルが 2 本あって 1 行で済ませたいときに輝きます。
ファミリー全体で使える便利な機能が 2 つあります。関数の後に書いた追加の引数は、毎回の呼び出しでその関数に転送されます。
そして、既製の関数がぴったり合わないときはいつでも無名関数を差し込めます — sapply(x, \(v) max(v) - min(v)) は、ヘルパーに名前を付けることなく要素ごとの範囲を計算します。
apply ファミリー vs for ループ
R では for ループ が遅く、apply ファミリーが速い、という話を耳にするでしょう。それはほぼ俗説です。本当に遅いのは ループの中で結果を伸ばすこと です — result <- c(result, new_value) は毎回ベクトル全体をコピーします。出力をあらかじめ確保するループの性能は問題ありません。
したがって、選択の基準は性能ではなく読みやすさです。apply 版は 何をするか を 1 行で述べ(「各要素を 2 乗する」)、領域確保も代わりにやってくれます。だからこそ、素直な要素ごとの処理にはこれが定石なのです。for ループは、繰り返しが前の結果に依存するとき、break による早期脱出が必要なとき、本体が長くてラムダにすると害の方が大きいときに真価を発揮します。どちらも正当な R です。
この記事のまとめ
- ファミリー全体は 1 つの考え方です。すべての要素に関数を実行し、結果を集めること。
applyは行列の行(MARGIN = 1)と列(MARGIN = 2)用です。専用関数があるならrowSums/colMeansを優先しましょう。lapplyは常にリストを返し、sapplyは可能なら簡略化します — つまり戻り値の型がデータによって変わりえます。vapplyは戻り値の型を固定します。驚きがあってはならないコードで使いましょう。mapplyは複数の入力をまとめて対応付け、tapplyはグループ内で値を集約します。- for ループは本質的に遅いわけではありません。遅いのはベクトルを伸ばすことです。最も読みやすい書き方を選びましょう。
次は パイプ — これらの呼び出しを読みやすい段階的なパイプラインにつなぐ演算子です。
よくある質問
R の lapply と sapply の違いは何ですか?
lapply は例外なく常にリストを返します。sapply は同じ計算を実行したうえで結果を簡略化しようとします — すべての要素の長さが 1 ならベクトルに、長さが共通なら行列に、簡略化できなければリストのままです。対話的な作業では sapply が快適ですが、スクリプトでは戻り値の型が変わらない lapply(または vapply)の方が安全です。
R の apply() は何をしますか?
apply(m, MARGIN, FUN) は行列に対して FUN を実行します。MARGIN = 1 なら各行に、MARGIN = 2 なら各列に適用します。apply(m, 1, sum) は行ごとの合計、apply(m, 2, mean) は列ごとの平均を返します。これは行列や配列のためのものです。リストやベクトルには lapply / sapply を使いましょう。
R では apply ファミリーは for ループより速いですか?
たいていは大差ありません — それは俗説です。結果ベクトルをあらかじめ確保した、よく書かれた for ループは同等の性能を出します。for ループの評判を落としたのは、結果を 1 要素ずつ伸ばし、そのたびに全体をコピーするループです。apply ファミリーは速度ではなく、簡潔さと意図の表現のために選びましょう。
R の vapply は何に使いますか?
vapply は契約付きの sapply です。各結果の型と長さを宣言します。例: vapply(x, nchar, integer(1))。関数がそれ以外を返した場合、R は予期しない構造を黙って渡す代わりに、その場でエラーを発生させます。無人で動き続ける必要のあるコードではこちらを選びましょう。