Python
R

Rのdo.call()はクラシカルな高次関数か?使い方を学ぶ

More than 1 year has passed since last update.

Kaggleの参加者コードを紹介するKaggle kernelsを見ていると do.call() を多用するRコードを見かけた.do.call() はほとんど初見だったので調べてみたところ,割とクラシカルな関数で,使い方も難しくないようだ.以下,忘れないようにメモしておく.

do.call()の概要

まずは,CRANマニュアルから引用する.

do.call - Execute a Function Call

Description

do.call constructs and executes a function call from a name or a function and a list of arguments to be passed to it.

Usage
do.call(what, args, quote = FALSE, envir = parent.frame())

Arguments
- what either a function or a non-empty character string naming the function to be called.
- args a list of arguments to the function call. The names attribute of args gives the argument names.
- quote a logical value indicating whether to quote the arguments.
- envir an environment within which to evaluate the call. This will be most useful if what is a character string and the arguments are symbols or quoted expressions.

機能としては,ずばり「関数呼び出し」(Function Call)である.R言語には,充実のApply関数群を備えているのでそちらが有名のような気がするが,このdo.call()もケースに応じて使われているようである.引数は上記の通り4つ取るようだが,必須なのは最初の2つ,関数オフジェクトの "what" とそれに渡す引数 "args" である."args"は,リスト変数とする必要がある.

少し使用例を示す.

まず,関数を定義する.

# define my own function
myrange <- function (larg) {
    nv <- unlist(larg)
    rg <- max(nv) - min(nv)
    return(rg)
}

ここでは,Rですぐに参照できる"iris"を使う.

# Data.Frame example
head(iris)

Table 1. Iris Dataset
r_do_call1.PNG

定義した関数 "myrange" をdo.call()する.

do.call(myrange, list(iris$Sepal.Length))
# Out: 3.6

期待通り Sepal.Lengh の最大値 - 最小値の値(3.6)が出力された.
一応,Rビルトインの range() で計算すると,4.3,7.9(最小値,最大値)となったので,上記 3.6(= 7.9 - 4.3) と解は一致している.

もう一例,確認する.まず,数値をnormalizeする関数を用意する.入力データサンプルを用意して,以下のように do.call() を実行する.

normalize <- function(x, m=mean(x), s=sd(x)) {
    (x - m) /s
}

myseq = list(c(1, 3, 6, 10, 15))
do.call(normalize, myseq)

# -1.0690449676497 -0.712696645099798 -0.17817416127495 0.534522483824849 1.4253932901996

出力された数値リストの平均と標準偏差は,

mean of normalized =
[1] -5.572799e-18
standard deviation = 
[1] 1

のように0近傍値と1であることから,期待したnormalizeが実行できていることが分かる.

Python Pandasのapply()と比較する

Rのdo.call()に近いのはPythonビルトイン関数のmap()のような気もするが,個人的にあまり使わないので,今回は,Pandasのapply()と比較する.(参考: "Python for Data Analysis" - O'reilly media)
まず,サンプルデータを用意.

# Sample Data
frame = pd.DataFrame(np.random.randn(4,3), columns=list('bde'),
                    index=['Utah', 'Ohio', 'Texas', 'Oregon'])
frame

** Table 2. Data Example**
do_call_py2.PNG

レンジ(最大値 - 最小値)を算定する関数を用意して,それをpd.DataFrameにapply() する.

# define lambda function
f = lambda x: x.max() - x.min()
frame[['d']].apply(f)
# if I execute frame['d'].apply(f), error is raised. "apply()" is for pd.DataFrame

これで期待した動作となる.

Out: d    4.016529
dtype: float64

カラム指定を数値で行う場合は,iloc[]を使う以下のパターン.

frame.iloc[:, [2]].apply(f)

# Out: e    2.160329
# dtype: float64

注意点としては,シーケンスを所定関数にわたしたいので,frame[['d']]frame.iloc[:, [2]] のようにカラム指定をリストで行わなければならないことである.(これを frame['d'], frame.iloc[:, 2] にすると,pd.Seriesオブジェクトに対するapply(),スカラー要素ごとの処理と解釈され,errorとなる.)

これで先ほどのR,do.call()と同じ動作が実現できた.

まとめ

do.call() はあまり見かけない関数だが(私だけ?),「data.frame に対して処理を行ったのちにまとめる」という状況で使われるようである.但し,Apply関数群の方が便利で,do.call()は「クラシカル」な書き方であるように見える.個人的には積極的にdo.call()を使おうと思わないが,人のコードのdo.call()を見たときに,あわてずにきちんと理解したいと思う.

Pythonで do.call() にあたるものは見当たらないが,Pandasのapply(),もしくは(データをばらして)リスト内包表記による処理を行うことより,所望の動作が実現できると思われる.

(R は,ver. 3.3.1 (on jupyter notebook), Python は,ver. 3.5.2 (on jupyter notebook) を用いました.)

参考文献