describe_vec <- function(x, na.rm = TRUE) {
list(
mean = mean(x, na.rm = na.rm),
sd = sd(x, na.rm = na.rm),
n = sum(!is.na(x))
)
}
describe_vec(c(1, 2, 3, NA, 5))$mean
[1] 2.75
$sd
[1] 1.707825
$n
[1] 4
分類: 枝(知っていると強い)
依存関係: U0 → U1 → U23
使用データ: iris(R内蔵)、BaseballDecade.csv
学習目標:
if/else、for、whileによる制御構造を使えるapply()族(apply、lapply、sapply)でループを置き換えられるpurrr::map()族を使って関数型プログラミングスタイルでデータを処理できるRにはたくさんの組み込み関数(mean()、sum()、sd()など)がありますが、自分で関数を定義することもできます。自作関数を使うと、同じ処理を何度も書かずに済み、コードの見通しがよくなります。
function()の中に引数を定義し、波括弧{}の中に処理を書きます。return()で値を返します。return()を省略した場合、最後に評価された式の値が自動的に返されます。
$mean
[1] 2.75
$sd
[1] 1.707825
$n
[1] 4
この関数は、平均・標準偏差・有効ケース数をリストにまとめて返します。na.rm = TRUEはデフォルト引数で、呼び出し時に省略するとTRUEが使われます。
関数定義時に引数名 = 値と書くと、デフォルト値を設定できます。呼び出し時にその引数を指定しなければデフォルト値が使われます。
関数の中で作られた変数はローカル変数であり、関数の外からはアクセスできません。一方、関数の外で作られた変数はグローバル変数で、関数の中からも参照できます(ただし、関数内での変更はグローバル変数に影響しません)。
この仕組みにより、関数の中で使う変数名が外の変数と衝突する心配がなくなります。
条件に応じて異なる処理を行うにはif/elseを使います。
[1] "合格"
[1] "追試"
[1] "不合格"
ifの条件は長さ1の論理値(TRUEまたはFALSE)でなければなりません。ベクトル全体に条件を適用したい場合はifelse()を使います。
forループは、ベクトルの各要素に対して処理を繰り返します。
whileループは、条件がTRUEである限り処理を繰り返します。繰り返し回数が事前に分からない場合に使います。
[1] 128
[1] 7
whileループは無限ループに注意が必要です。条件がFALSEになるように必ず更新処理を入れましょう。
FizzBuzzは古典的なプログラミング問題です。1から順に数を出力し、3の倍数なら”Fizz”、5の倍数なら”Buzz”、15の倍数なら”FizzBuzz”を出力します。
[1] "1" "2" "Fizz" "4" "Buzz" "Fizz"
[7] "7" "8" "Fizz" "Buzz" "11" "Fizz"
[13] "13" "14" "FizzBuzz" "16" "17" "Fizz"
[19] "19" "Buzz"
フィボナッチ数列は、最初の2つが1で、以降は直前の2つの和で定義される数列です。
[1] 1 1 2 3 5 8 13 21 34 55
Rではforループを書くことができますが、ベクトルや行列に対して同じ処理を繰り返す場合、apply族の関数を使った方が簡潔で高速です。
apply(X, MARGIN, FUN)は行列Xに対して、MARGIN = 1なら行ごと、MARGIN = 2なら列ごとに関数FUNを適用します。
lapply(X, FUN)はリスト(またはベクトル)Xの各要素に関数FUNを適用し、結果をリストで返します。
sapply(X, FUN)はlapply()と同じですが、結果を可能であればベクトルや行列に簡略化して返します。
Sepal.Length Sepal.Width Petal.Length Petal.Width
5.843333 3.057333 3.758000 1.199333
sapply()は便利ですが、入力データによって返り値の型が変わることがあるため、プログラム中ではlapply()やvapply()の方が安全です。
purrr::map(.x, .f)はlapply()のtidyverse版です。同じくリストの各要素に関数を適用しますが、ラムダ式(~による無名関数)が使えるなど、記法が洗練されています。
| 関数 | 返り値の型 |
|---|---|
map() |
リスト |
map_dbl() |
数値ベクトル |
map_chr() |
文字列ベクトル |
map_int() |
整数ベクトル |
map_lgl() |
論理値ベクトル |
map_df() |
データフレーム(行結合) |
型指定版を使うと、結果の型が保証されるため、予期しないエラーを防げます。
[1] 0.07138289 0.56858983 0.74688439
このコードは以下の手順で処理しています:
group_split(Species): Speciesごとにデータを3つのデータフレームに分割map(~ lm(...)): 各データフレームに対して回帰分析を実行map(summary): 各結果のsummaryを取得map_dbl(~ .x$r.squared): 各summaryからR二乗値を数値として抽出~はラムダ式(無名関数)の書き方で、.xは各要素が入るプレイスホルダーです。
Rはベクトル演算に最適化されているため、forループよりもapply/mapの方が一般的に高速で、コードも簡潔になります。
Sepal.Length Sepal.Width Petal.Length Petal.Width
5.843333 3.057333 3.758000 1.199333
Sepal.Length Sepal.Width Petal.Length Petal.Width
5.843333 3.057333 3.758000 1.199333
Rで自作関数を定義するとき、使うキーワードはどれですか?
Rではfunctionキーワードを使って関数を定義します。Pythonではdef、JavaScriptではfunctionを使いますが、Rでもfunctionです。
関数の中でreturn()を省略した場合、何が返されますか?
Rの関数では、return()を省略すると、関数本体の最後に評価された式の値が自動的に返されます。
ただし、途中で処理を打ち切って値を返したい場合は、明示的にreturn()を使う必要があります。
関数の中で作成された変数は、関数の外からアクセスできる。
関数内で作られた変数はローカル変数であり、関数の外からはアクセスできません。これをスコープ(変数の有効範囲)と呼びます。
この仕組みにより、関数内部の変数が外部と干渉することを防いでいます。
forループとwhileループの主な違いは何ですか?
forループ: ベクトルの各要素に対して処理を繰り返します。反復回数はベクトルの長さで決まります。whileループ: 条件がTRUEである間、処理を繰り返します。反復回数は事前に分からないことがあります。apply(mat, 1, sum)の1は何を意味しますか?
apply()の第2引数MARGINは、関数を適用する方向を指定します。
MARGIN = 1: 行方向(各行に関数を適用)MARGIN = 2: 列方向(各列に関数を適用)覚え方: 行列は「行→列」の順で添字を指定するので、1が行、2が列です。
purrr::map()とlapply()の主な違いは何ですか?
map()とlapply()は基本的に同じ動作をしますが、purrr::map()には以下の利点があります:
~記法): ~ .x + 1のように簡潔に無名関数を書けるmap_dbl()、map_chr()、map_df()などで返り値の型を保証できるpossibly()、safely()などとの連携が容易Rのラムダ式(無名関数)~ .x + 1はどのように書き換えられますか?
purrr の~記法は、function(.x, .y, ...) { ... }の省略形です。
~の後に.xで第1引数、.yで第2引数を表します。map()では引数が1つなので.xのみを使います。
関数定義でfunction(x, na.rm = TRUE)と書いたとき、na.rm = TRUEは何を意味しますか?
na.rm = TRUEはデフォルト引数です。関数を呼び出すときにこの引数を省略すると、デフォルト値のTRUEが使われます。
平均値、中央値、標準偏差を同時に計算して返す関数desc_stats()を作成してください。引数には数値ベクトルを取り、結果は名前付きリストで返してください。
mean()、median()、sd()を使い、list()でまとめて返します。欠損値対策としてna.rm引数も付けると親切です。
返り値をリストにすることで、複数の異なる型の情報をまとめて返すことができます。データフレームの1行として返す方法(data.frame()を使う)もあります。
FizzBuzz問題をforループで解いてください。1から30までの数について、15の倍数なら”FizzBuzz”、3の倍数なら”Fizz”、5の倍数なら”Buzz”、それ以外はその数字を文字列として格納する関数を作成してください。
%%は剰余演算子で、i %% 3 == 0は「iが3で割り切れる」を意味します。判定の順序に注意してください。15の倍数は3の倍数でもあり5の倍数でもあるので、15の判定を最初に行う必要があります。
ポイント: result <- character(n)で事前にベクトルのサイズを確保しています。forループ内でc()を繰り返して結合するよりも、事前にサイズを確保する方がメモリ効率が良く高速です。
irisの数値列(1列目から4列目まで)について、sapply()を使って各列の平均値を求めてください。
iris[, 1:4]で数値列だけを取り出し、sapply()にmean関数を渡します。
sapply()はリストやデータフレームの各要素(列)に対して関数を適用し、結果をベクトルに簡略化して返します。forループで書くと4行以上必要になる処理が、1行で完結します。
カレントディレクトリにある複数のCSVファイルのパスがベクトルfile_pathsに格納されているとします。lapply()を使って、これらのファイルをすべて読み込み、リストとして保持するコードを書いてください。
lapply(file_paths, readr::read_csv)で各ファイルを読み込めます。実際に実行する場合はファイルが存在する必要があります。
複数のCSVファイルを一括で読み込む場面は実務でよくあります。lapply()を使えば、ファイル数がいくつであっても同じコードで対応できます。
入力が数値ベクトルでない場合にエラーメッセージを返す関数safe_mean()を作成してください。is.numeric()で入力をチェックし、数値でなければstop()でエラーを発生させてください。
stop("メッセージ")でエラーを発生させます。関数の先頭で入力チェックを行うのが一般的なパターンです。
関数の先頭で入力を検証する(ガード節)パターンは、予期しないエラーの原因を特定しやすくするために重要です。stopifnot(is.numeric(x))という書き方もあります。
次のforループをpurrr::map_dbl()を使って書き換えてください。
map_dbl()に列番号のベクトルと、各列の標準偏差を計算するラムダ式を渡します。あるいは、データフレームを直接渡すこともできます。
forループ版では5行かかった処理が、map_dbl()では1行で書けます。返り値が数値ベクトルであることも型指定により保証されています。
BaseballDecade.csvを読み込み、球団(team)ごとに年俸(salary)を目的変数、身長(height)を説明変数とする回帰分析を実行してください。purrr::map_df()を使って、各球団の回帰係数(切片と傾き)をデータフレームにまとめてください。
dplyr::group_split()でデータを球団ごとに分割し、map_df()で各グループに対してlm()を実行します。結果をbroom::tidy()でデータフレームに変換すると便利です。broomパッケージがない場合はcoef()で係数を取り出して手動でdata.frame()を作ります。
group_split()はgroup_by()と違い、データフレームのリストを返します。これをmap_df()に渡すことで、各グループに対して同じ処理を適用し、結果を1つのデータフレームに結合できます。
クロージャ(関数を返す関数)を使って、べき乗関数を生成する関数make_power()を作成してください。make_power(2)は2乗する関数を、make_power(3)は3乗する関数を返すようにしてください。
関数の中で別の関数を定義してreturn()で返します。外側の関数の引数は、内側の関数から参照できます(レキシカルスコープ)。
これはクロージャと呼ばれるパターンです。make_power(2)を呼び出すと、n = 2という環境を記憶した新しい関数が生成されます。関数型プログラミングの重要な概念で、パラメータ化された処理を柔軟に生成できます。
3行4列の行列を作成し、apply()を使って行ごとの合計と列ごとの平均を計算してください。
matrix()で行列を作り、apply(mat, 1, sum)で行合計、apply(mat, 2, mean)で列平均を計算します。
MARGIN = 1で行方向、MARGIN = 2で列方向に関数を適用します。行列データに対する集計で頻繁に使うパターンです。なお、行合計や列合計についてはrowSums()、colSums()、rowMeans()、colMeans()という専用関数もあり、こちらの方が高速です。
forループ、sapply()、purrr::map_dbl()の3つの方法で、irisの数値列(1〜4列)の標準偏差を計算し、system.time()で実行時間を比較してください。
正確な比較をするためには、十分な回数(例えば10000回)繰り返すと差が見えやすくなります。replicate()を使うと便利です。
この程度の小さなデータでは大きな差は出ませんが、データが大きくなるにつれてsapply()やmap_dbl()の方がforループより効率的になる傾向があります。ただし、Rのパフォーマンスで最も重要なのはベクトル化された関数を使うことであり、forループかmap系かの差は実用上は小さいことが多いです。
課題: 以下のforループを使ったコードを、AIに相談しながらpurrr::map()系の関数を使ってリファクタリング(書き直し)してください。
# BaseballDecade.csvを読み込み、年度ごとの要約統計量を計算する
df <- readr::read_csv("../data/BaseballDecade.csv")
years <- unique(df$Year)
results <- list()
for (i in seq_along(years)) {
sub_df <- df[df$Year == years[i], ]
results[[i]] <- data.frame(
Year = years[i],
n = nrow(sub_df),
mean_salary = mean(sub_df$salary, na.rm = TRUE),
sd_salary = sd(sub_df$salary, na.rm = TRUE),
max_salary = max(sub_df$salary, na.rm = TRUE)
)
}
final <- do.call(rbind, results)AIとの対話を通じて:
map_df()やgroup_by() + summarise()で書き換える方法を検討する提出物: AIとの対話ログ、リファクタリング後のコード、考察。
課題: BaseballDecade.csvを分析するための「カスタム分析パイプライン関数」を、AIと協力して設計・実装してください。
この関数は以下の仕様を満たしてください:
AIに相談しながら:
BaseballDecade.csvでテストする提出物: AIとの対話の記録、完成した関数のコード、テスト結果、設計上の工夫点。
このユニットでは、Rのプログラミング基礎概念を学びました:
function()で自作関数を作り、処理を再利用できる。デフォルト引数やスコープを理解することで、堅牢な関数が書けるif/elseで条件分岐、for/whileで繰り返し処理ができるapply()、lapply()、sapply()でループを簡潔に書き換えられるコード自体はAIに書いてもらえる時代ですが、「どういう関数を作るべきか」「どのように問題を分解するか」というアルゴリズム的思考は、AIに的確な指示を出すためにも不可欠です。
進捗: あなたは今 23-C-8 まで完了しました!(と仮定)次は 23-B-1 に進みましょう。