ほくそ笑む

R言語と統計解析について

統計言語 R 探訪 match.arg() 編

統計言語 R の関数で、選択肢を受け取る引数を持つものがあります。
例えば、t.test() の Usage(使い方)を見ると、次のように書いてあります。

t.test(x, y = NULL,
       alternative = c("two.sided", "less", "greater"),
       mu = 0, paired = FALSE, var.equal = FALSE,
       conf.level = 0.95, ...)

この alternative という引数に注目です。

       alternative = c("two.sided", "less", "greater"),

と書いてありますが、これは alternative という引数には "two.sided", "less", "greater" のどれかを渡してほしいということを表しています。
これは R の関数を使うときのお約束事なので、覚えておいて損は無いです。
ところで、これら期待された文字列以外を引数に渡してみると、どうなるんでしょう?

t.test(c(1,2,3,4,5), alternative="one.sided")

を実行してみると、

Error in match.arg(alternative) : 
  'arg' should be one of “two.sided”, “less”, “greater”

エラーが出ました。どうやら期待された文字列以外は受け付けないようです。
もうひとつ、このような引数のお約束事として、期待された文字列のうち、一つを特定できる最初の数文字を渡しても OK ということも挙げられます。
例えば上記の例でいえば、"two.sided" は "t" で特定できる("less" は "l", "greater" は "g")ので、次のようにしても実行できます。

t.test(c(1,2,3,4,5), alternative="t") # alternative="two.sided" と同じ

これらの動作はもちろん t.test() 関数内に書かれているのでしょう。
引数一つにこれらの処理をいちいち施さなくてはならないとは、関数を作成するのも一苦労ですね。
では、ちょっと、t.test() 関数内をのぞいてみましょう。

function (x, y = NULL, alternative = c("two.sided", "less", "greater"), 
    mu = 0, paired = FALSE, var.equal = FALSE, conf.level = 0.95, 
    ...) 
{
    alternative <- match.arg(alternative)
    (省略)
}

な、なんと、これらの動作はたったの一行

    alternative <- match.arg(alternative)

だけで実現されているのです。
あれ? alternative の初期値はどこいったの? なんでこれでうまくいくの? と思ったあなた、興味がわいてきましたか?
これから一緒に match.arg() の探訪に出かけてみましょう。

match.arg() 探訪

match.arg() の中身は、R に match.arg と打ち込めば見ることができます。
ここでは、中核となる部分だけを抜粋してみましょう。

function (arg, choices, several.ok = FALSE) 
{
    if (missing(choices)) {
        formal.args <- formals(sys.function(sys.parent()))
        choices <- eval(formal.args[[deparse(substitute(arg))]])
    }
    (省略)
    i <- pmatch(arg, choices, nomatch = 0L, duplicates.ok = TRUE)
    if (all(i == 0L)) 
        stop(gettextf("'arg' should be one of %s", paste(dQuote(choices), 
            collapse = ", ")), domain = NA)
    i <- i[i > 0L]
    (省略)
    choices[i]
}

それでは動作を追っていきます。
まず最初に、choices は入れてませんから、if 文の中に入り、

        formal.args <- formals(sys.function(sys.parent()))

が実行されます。
この formals(sys.function(sys.parent())) は定型文で、呼び出し元の関数の引数のデフォルト値をリストで返します。
今回はこんな感じになります。

$x


$y
NULL

$alternative
c("two.sided", "less", "greater")

$mu
[1] 0

$paired
[1] FALSE

$var.equal
[1] FALSE

$conf.level
[1] 0.95

$...

次に、

        choices <- eval(formal.args[[deparse(substitute(arg))]])

が実行されます。
ここで、substitute() は元の式を抽出する関数で、match.arg(alternative) と入れたので、alternative が得られます。
得られた alternative はまだ式の状態なので、deparse() を使って文字列に変換しています。
つまり、deparse(substitute(arg)) で、match.arg() に入力された変数名を取得しています。
ここら辺は詳しく説明しませんが下記の実行結果でなんとなく納得してください。

func <- function(arg) {
  return(deparse(substitute(arg)))
}
func(1 + 2)
x <- 1
func(x)
alternative <- "two.sided"
func(alternative)

[1] "1 + 2"
[1] "x"
[1] "alternative"

さて、formal.args[ ["alternative"] ] には c("two.sided", "less", "greater") が入っていました。
これはまだ式の状態なので、eval() で評価して choices に代入しています。
以上で、choices には元の関数のデフォルト値が入りました。結構おもしろいからくりだと思います。
残りの部分も見ていきましょう。

    i <- pmatch(arg, choices, nomatch = 0L, duplicates.ok = TRUE)

pmatch() 関数は、第二引数の中から、第一引数にマッチする文字列が何番目にあるかを返します。
例えば、

pmatch("t", c("two.sided", "less", "greater"), nomatch = 0L, duplicates.ok = TRUE)

[1] 1

ちなみに第一引数がベクトルの場合、それぞれの結果のベクトルが返されます。

pmatch(c("t", "g"), c("two.sided", "less", "greater"), nomatch = 0L, duplicates.ok = TRUE)

[1] 1 3

つまり、match.arg() は複数選択の場合にも使えるということです。複数選択可能にしたい場合は、several.ok = TRUE にすればよいです。
nomatch = 0L としているので、マッチするものが無かった場合には 0 が返ります。

pmatch(c("a", "b", "c"), c("two.sided", "less", "greater"), nomatch = 0L, duplicates.ok = TRUE)

[1] 0 0 0

pmatch() の返り値が全部 0 だった場合は、選択肢にないよということで、次のようにエラーにしています(この記事の最初に見たやつですね)。

    if (all(i == 0L)) 
        stop(gettextf("'arg' should be one of %s", paste(dQuote(choices), 
            collapse = ", ")), domain = NA)

i のうちマッチしたもの(i > 0L)だけ抽出して、choices[i] を返せば完了です。*1

    i <- i[i > 0L]
    (省略)
    choices[i]

これで、あらかじめ決められた選択肢の中から選んだものを返す関数ができたというわけです。おもしろいですね。

まとめ

match.arg() 関数を使えば、選択肢を受け取る引数を簡単に実現できます。

func <- function(select = c("しょうゆ", "しお", "ソース")) {
    select <- match.arg(select)
    str <- paste("目玉焼きにはやっぱり", select, "だよねー")
    return(str)
}

R の関数を探訪するのはおもしろいですね。
みなさんもぜひやってみてください。
次に探訪されるのは、そう、あなたの使っているその関数かもしれません。

*1:省略部分は、複数選択不可の場合にマッチが複数あるときのエラー処理です