Rで自由な関数を作成するために覚えておきたいこと
Rの自作関数で自由な解析ツールを手に入れる
はじめに
こんにちは。今回はRで関数を作る方法と、その効果について解説していきます。
そもそも関数とは何かという話ですが、ざっくり言うと 複数行の処理をまとめたもの 、また、 使い方がわかりにくい関数のインターフェースを再定義したもの(ラッパー関数) ということになります。
基本的に、パッケージ等で配布されている関数は大まかに以下の特徴があります。
- 外部のデータを操作しやすい形式に変形して取り込む(etc.. leadxl, sf)
- 特定の(単体)処理をおこなう(etc.. ggplot2, stringr)
既存の関数やパッケージはあくまで操作を便利にするツールであり、その道具を使ってどのような処理をしたいのか、という部分は自らソースコードを記述し、処理を簡単にするためには新たに関数を定義する必要が出てきます。
また、一言に関数と言っても、以下のように大きく3つのパターンが考えられます。
- base 関数や パッケージの関数を使って処理をひとまとめにした関数
- 関数を作成する関数
- メタプログラミングを使ってコードを自動生成する関数
使う頻度が多いのは 1. だと思いますが、その他の関数も使い方を覚えれば強力なツールになります。
それぞれ順番に紹介します。
なお、このページでは、Rでデータ解析を実施するにあたり、特に前処理を苦手としている方や、内部処理を理解した上で、自ら関数を組み上げてツールを作ってしまうほうが理解が早い方には参考になるような内容を紹介しています。
処理をひとまとめにした関数
例えば、文字列から日付型にデータを変換したい場合の処理は以下のようになります。
inputTxt <- "2023-02-01"
test1 <- try(as.Date(inputTxt), silent = FALSE)
test1
[1] "2023-02-01"
この場合は問題ありませんが、他の文字列を間違って使ってしまった場合、以下のようになり処理が止まってしまいます。
inputTxt <- "abcde"
test1 <- try(as.Date(inputTxt), silent = FALSE)
Error in charToDate(x) :
文字列は標準的な曖昧さのない書式にはなっていません
test1
[1] "Error in charToDate(x) : \n 文字列は標準的な曖昧さのない書式にはなっていません \n"
attr(,"class")
[1] "try-error"
attr(,"condition")
<simpleError in charToDate(x): 文字列は標準的な曖昧さのない書式にはなっていません >
そこで、エラー処理の部分を含めて関数化して、エラーにも対処できるようにします。
chr2date <- function(inputTxt) {
x <- try(as.Date(inputTxt), silent = FALSE)
if (class(x) == "try-error") {
y <- inputTxt
} else {
y <- x
}
y
}
str(chr2date("2023/02/04"))
Date[1:1], format: "2023-02-04"
str(chr2date("abcde"))
Error in charToDate(x) :
文字列は標準的な曖昧さのない書式にはなっていません
chr "abcde"
このままオブジェクトに代入してもエラーは表示されず、文字列は文字列のまま代入されます。
test2 <- chr2date("abcde")
Error in charToDate(x) :
文字列は標準的な曖昧さのない書式にはなっていません
test2
[1] "abcde"
基本的に関数は処理毎に小さく作成することがおすすめです。確認・修正がすぐにできることと、使い回しができるようになるためです。
作成した chr2date
関数も、引数をベクトル指定できるように修正すると更に便利になりそうです。このように修正案も想像しやすくなります。
また、繰り返し処理などでは処理が重くなり時間がかかることもあります。その場合は compiler
パッケージの cmpfun()
関数を使うことでバイトコンパイルすることができ、Rが実行のたびにコンパイルすることを避けられて処理の高速化に繋がります。
chr2date_compiled <- compiler::cmpfun(chr2date)
ラッパー関数の作成
前回紹介した tapply
のラッパー関数である tapply2
で解説します。
tapply2 <- function(df, group, var, f) {
d <- lapply(df[var], function(m) {
tapply(m, list(df[, group]), f, na.rm = TRUE)
})
do.call(rbind, d)
}
行われている処理は tapply
とほぼ同じですが、第一引数で指定したデータフレーム列名を第2,3引数で使用できるようにした点が違います。
data <- iris
str(data)
'data.frame': 150 obs. of 5 variables:
$ Sepal.Length: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
$ Sepal.Width : num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
$ Petal.Length: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
$ Petal.Width : num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
$ Species : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
tapply(data$Sepal.Length, data$Species, mean, na.rm = TRUE)
setosa versicolor virginica
5.006 5.936 6.588
tapply2(data, "Species", c(names(data[1:4])), mean)
setosa versicolor virginica
Sepal.Length 5.006 5.936 6.588
Sepal.Width 3.428 2.770 2.974
Petal.Length 1.462 4.260 5.552
Petal.Width 0.246 1.326 2.026
使いやすさは上がっていると思います。
もともと便利な dplyr
パッケージがあるのでそれを使えば良いのでしょうが、どんな環境でも使える、base関数だけで定義できる自作関数を準備しておくと便利なこともあります。
関数を生成する関数
何を言っているのかわからない。筆者は最初そう思いました。関数の定義が変動的になることなんてあるのだろうかと。
ここでのコードは、以下のように function()
を function()
で覆って定義します。
f <- function() {
function() { }
}
作成例ですが、次のように数値丸めの round
関数で解説します。
round2 <- function(num) {
function(x) {
round(x, num)
}
}
引数numで丸める桁数を指定すると、その桁数で指定した round
関数を作成してくれます。
ポイントとしては、外側の関数定義と内側関数定義で使用している引数がそれぞれ違うところです。
こうすることで、条件に合わせてround
関数を生成することができるようになりました。
f1 <- round2(1)
f3 <- round2(3)
f1(pi)
[1] 3.1
f3(pi)
[1] 3.142
このような関数定義が活きるのは、繰り返し処理の中で、同じオブジェクト名を使用して異なる挙動をするようにしたい場合だと思います。
筆者は、一度書いたプログラムは二度と書きたくないと思うので、条件分岐の中でもどうにかして楽をしたいと思っています。
そんなときはこの方法がぴったりです。
例えば、関数を以下のように修正して、前章で作成した tapply2
で出力される要約データに適用してみます。
round3 <- function(set_unit) {
num <- switch(set_unit,
"setosa" = 1,
"versicolor" = 2,
"virginica" = -1)
function(x) {
round(x, num)
}
}
data <- iris
d <- tapply2(data, "Species", names(data[1:4]), mean)
for (i in colnames(d)) {
rd <- round3(i)
print(rd(d[, i]))
}
Sepal.Length Sepal.Width Petal.Length Petal.Width
5.0 3.4 1.5 0.2
Sepal.Length Sepal.Width Petal.Length Petal.Width
5.94 2.77 4.26 1.33
Sepal.Length Sepal.Width Petal.Length Petal.Width
10 0 10 0
まとめて処理ができました。 for
の中に直接条件分岐を書いても同じ結果は出ますが、このような関数は他の場面で使用する頻度が高いと思われるので、関数化しておいたほうが使い勝手は良いと思います。
筆者は定量分析を過去に行っていましたが、その際に数値を丸める処理をしていました。
定量分析の結果は 小数点以下x桁まで表示 等のルールが細かく決まっており、似たようなデータ処理の場面では役に立つ方法だと思いいます。
メタプログラミングによる関数定義
今度はプログラムコードを生成する関数を作成する方法です。
これこそ、一体何を言っているのか? AIによるソースコード自動生成のことを言っているのか? という感じですが、メタプログラミング自体は Lisp の時代から存在する手法です。
例えば、Rではダブルクオート""で囲まれた文字は文字列として扱われ、それ以外はオブジェクトとして扱われます。
それを、引数にダブルクオートを使わずに文字を入力し、それをそのまま文字列として認識したい場合はどうすればいいでしょうか?
x <- 123
f <- function(x) {
as.character(x)
}
f(x)
[1] "123"
これだと当たり前のように、事前にxに代入した 123
が出力されます。
では、以下のようにするとどうでしょうか?
x <- 123
f <- function(...) {
as.character(substitute(...))
}
f(x)
[1] "x"
f(12345)
[1] "12345"
文字列として出力できました。これは表現式という形式を用いた手法なのですが、今回は詳細を省き、概要だけお伝えします。
こうすることで何が便利かと言うと、いちいち関数の引数指定する際に文字列指定する " をつけなくても成り立つということです。
先に紹介した tapply2
を以下のように修正すると、更に入力が楽になりました。
tapply3 <- function(df, group, vars, f) {
g <- substitute(group)
d <- lapply(df[vars], function(m) {
tapply(m, list(df[, as.character(g)]), f, na.rm = TRUE)
})
do.call(rbind, d)
}
tapply3(iris, Species, 1:4, mean)
setosa versicolor virginica
Sepal.Length 5.006 5.936 6.588
Sepal.Width 3.428 2.770 2.974
Petal.Length 1.462 4.260 5.552
Petal.Width 0.246 1.326 2.026
ネームスペースを使うともっといい方法で定義できるのですが、今回は上記のように簡単なところだけ紹介しておきます。
メタプログラミングはプログラムコードを作成する手法です。そのため、使い方によっては全く別の言語を設計することもできます。
これから自前のパッケージを作ることを考えている方は、 quote, expression, eval, bquote 等の使い方を勉強してみてください。
まとめ
今回は関数作成の方法について概要をお伝えしました。
この内容だけで自由な関数設計ができるようになるとは思っていませんが、参考になればと思います。
なお、最後に触れた表現式については使いどころが大切です。筆者の説明より上手に、具体的に解説している書籍は数多くありますので参考にしてください。
特に、 Advanced R Second Editionは解説が分かりやすく、First Editionは邦訳されています。R言語徹底解説 共立出版 2016.
いろいろ試してみて、実際に動作を確認することが理解への第一歩です。これから触れてみる方は、時間をしっかり掛けて学んでください。
Discussion