R言語上級ハンドブック

R言語上級ハンドブックを一通り読了しました.

R言語上級ハンドブック

R言語上級ハンドブック

本書は,Rで効率的・効果的にデータ解析や処理を行うための中級〜上級のTips集です.著者はR界隈で著名な方々ばかりで,高度なトピックが平易に解説されています.内容も,実行パフォーマンス(処理速度,メモリ使用量等),Hadoopや他言語との連携,グラフィクスなど多岐に渡ります.また,knitrやslidifyによるレポート・プレゼン資料作成,shinyによるWebアプリ作成など,比較的新しい話題についても収録されています.
Rで分からないことをStack Overflowなどで調べているユーザにとって,本書は座右の書となることでしょう.

関数のメモ化,連長圧縮,Deducerパッケージなど,勉強になることが多かったのですが,特に印象に残った以下の3点についてメモします.

パッケージが提供する関数の仕様の変更("079 既存の関数を書き換える","018 パッケージ内のオブジェクトを変更する")

Rのパッケージを使用していると,関数の仕様を変更したいケースが多々発生します.これまではソースをコピー → 仕様を変更 → source関数で読み込む,という方法を取っていました.しかし,この方法はパッケージで公開されていない関数などを呼び出している場合に名前空間を明示しないとエラーが発生するなどの問題が起きて,予想以上に手間がかかっていました.

本書では,既存の関数を書き換えるためにいくつかの方法が示されていますが,ここではmethodsパッケージのinsertSource関数を用いる方法を試してみます.insertSource関数は,ファイルで定義された関数を読み込んで,パッケージの関数を変更します.例として,baseパッケージのpaste0関数を書き換えてみましょう.

> # デフォルトでは与えられた文字列の集合をセパレータを""として結合
> paste0("a", "b")
[1] "ab"
> # 新しいpaste0関数の定義を書き込むファイル
> filename <- tempfile()
> # 新しい定義のファイルへの書き込み(セパレータを改行コードに設定)
> writeLines("paste0 <- function(...) paste(list(...), collapse=\"\n\")", filename)
> # 関数の書き換え
> insertSource(filename, package="base", force=FALSE)
関数でないオブジェクトは現在挿入できません(トレース不能です): .packageName 

 変更された関数が trace() を通じて挿入されます: paste0 
> # 書き換えが成功したことの確認
> paste0("a", "b")
[1] "a\nb"
> # 念のため,関数の定義が変更されたことの確認
> paste0
Object of class "functionWithTrace", from source
function (...) 
paste(list(...), collapse = "\n")
<environment: namespace:base>

## (to see original from package, look at object@original)

このTipsを実践することにより,パッケージの関数の仕様を書き換える手間が減らせそうです.

data.tableパッケージを用いた高速検索・グループ化処理("034 「data.table」パッケージでデータフレームの拡張を行う")

前々から調べようと思って手つかずになっていたパッケージの一つが,このdata.tableでした.
本書では,data.tableパッケージの概要について,『「data.frame」の機能の大部分を継承しつつ,キーの設定,バイナリサーチを用いた高速な検索,グループ化による処理などをサポートしています』と説明されています.

ここでは,data.tableパッケージのvignetteの一つである"Introduction to the data.table package in R"から抜粋する形で,パッケージの概要について眺めてみます.

> library(data.table)
>
> 1. オブジェクトの生成とdata.frameからの変換
>
> # data.tableオブジェクトの作成(data.frameと全く同様)
> set.seed(123)
> DT <-  data.table(x=c("b","b","b","a","a"),v=rnorm(5))
> DT
   x           v
1: b -0.56047565
2: b -0.23017749
3: b  1.55870831
4: a  0.07050839
5: a  0.12928774
> # data.frameオブジェクトからdata.tableオブジェクトへの変換
> CARS <- data.table(cars)
> head(CARS)
   speed dist
1:     4    2
2:     4   10
3:     7    4
4:     7   22
5:     8   16
6:     9   10
> # メモリ上に保持されたdata.tableオブジェクトのリストの確認(どれもKEYは未設定)
> tables()
NAME NROW MB COLS KEY
[1,] CARS 50 1 speed,dist
[2,] DT 5 1 x,v
Total: 2MB
>
> # 2. キー
> # data.tableオブジェクトの特定の行へのアクセス
> DT[2, ]
   x          v
1: b -0.2301775
> DT[DT$x=="b",]
   x          v
1: b -0.5604756
2: b -0.2301775
3: b  1.5587083
> cat(try(DT["b",],silent=TRUE)) # 行名が付与されていないので,これはできない
Error in `[.data.table`(DT, "b", ) : 
  When i is a data.table (or character vector), x must be keyed (i.e. sorted, and, marked as sorted) so data.table knows which columns to join to and take advantage of x being sorted. Call setkey(x,...) first, see ?setkey.
> # キーとなる列名を設定(列"x"の値でレコードがソートされる)
> setkey(DT,x)
> DT
   x           v
1: a  0.07050839
2: a  0.12928774
3: b -0.56047565
4: b -0.23017749
5: b  1.55870831
> # data.tableオブジェクトのリストにもキーを確認
> tables()
NAME NROW MB COLS KEY
[1,] CARS 50 1 speed,dist
[2,] DT 5 1 x,v x
Total: 2MB
> DT["b",] # DT["b"]でも可
   x          v
1: b -0.5604756
2: b -0.2301775
3: b  1.5587083
> # 特定のキーの値に該当するレコードが複数存在する場合,mult引数で最初or最後のレコードを抽出可能
> DT["b",mult="first"]
   x          v
1: b -0.5604756
> DT["b",mult="last"]
   x        v
1: b 1.558708
> 
> # 以下,キー設定の効果の検証(data.frame vs. data.table)
> set.seed(123)
> grpsize <- ceiling(1e7/26^2) # レコード数: 10^6, グループ数: 676
> grpsize
[1] 14793
> # data.frameの作成
> tt <- system.time(DF <- data.frame(
+         x=rep(LETTERS, each=26*grpsize),
+         y=rep(letters, each=grpsize),
+         v=runif(grpsize*26^2),
+         stringsAsFactors=FALSE)
+       )
> tt
   ユーザ   システム       経過  
     1.148      3.068      4.224
> head(DF, 3)
  x y         v
1 A a 0.9568333
2 A a 0.4533342
3 A a 0.6775706
> tail(DF, 3)
         x y         v
10000066 Z z 0.8490227
10000067 Z z 0.6784250
10000068 Z z 0.9932539
> dim(DF)
[1] 10000068        3
> # data.frameから特定の条件を満たしたレコードを抽出
> tt <- system.time(ans1 <- DF[DF$x=="R" & DF$y=="h", ])
> tt
   ユーザ   システム       経過  
     0.552      0.168      0.724 
> head(ans1, 3)
        x y         v
6642058 R h 0.4380854
6642059 R h 0.9481632
6642060 R h 0.4784464
>
> # data.tableへの変換
> DT <-  data.table(DF)
> # キーの設定
> setkey(DT,x,y)
> # data.tableから特定の条件を満たすレコードの抽出
> ss <- system.time(ans2 <- DT[J("R","h")])
> # data.tableはバイナリサーチを用いて高速に検索(360倍以上)
> ss
   ユーザ   システム       経過  
     0.004      0.000      0.002 
>
> 3. 高速なグループ化
> # 列名"V"の値の総和
> DT[, sum(v)]
[1] 4999521
> # 列名"x"のカテゴリごとの列名"V"の値の総和
> DT.sum.by.x <- DT[,sum(v),by=x]
> head(DT.sum.by.x)
   x       V1
1: A 192322.6
2: B 192110.5
3: C 192099.1
4: D 192189.3
5: E 192504.4
6: F 192246.1
> tail(DT.sum.by.x)
   x       V1
1: U 192135.4
2: V 192365.8
3: W 192560.9
4: X 192111.3
5: Y 192238.0
6: Z 192299.6

このパッケージには上記以外のvignetteも用意されているので,目を通したいところです.

また,どのように実装されているか気になるので,余裕があるときに中身を調べたいと思います.

TAGSファイルの作成("078 TAGSファイルを作成する")

Rを使用してしばらく経ちパッケージの関数で分からないことがあると,ソースコードを見にいくことが増えてきます.そのようなとき,まず始めに関数の依存関係を調べていましたが,R部分のコードについてはdebug/debugonce関数などを用いて地道に行っていました.

しかし,TAGSファイルを作成すれば,このような作業の負荷が軽減できそうです.

例として,先のdata.tableパッケージのソースコードに対してTAGSファイルを作成してみます.data.tableパッケージのソースコードをCRANから取得して解凍すると,次のディレクトリ構成となっています.

$ ls
DESCRIPTION  MD5  NAMESPACE  NEWS  R  inst  man  src  tests  vignettes

以下のコマンドを実行することにより,ディレクトリの直下に"TAGS"という名前のファイルが作成されます.

$ R CMD rtags

Tagging R/C/Rd files under /home/sfchaos/Documents/applitest/R/data.table/data.table; writing to TAGS (overwriting)...

etags: no input files specified.
	Try `etags --help' for a complete list of options.
etags: no input files specified.
	Try `etags --help' for a complete list of options.
etags: no input files specified.
	Try `etags --help' for a complete list of options.
etags: no input files specified.
	Try `etags --help' for a complete list of options.
Done
$ ls # TAGSファイルが作成されている
DESCRIPTION  MD5  NAMESPACE  NEWS  R  TAGS  inst  man  src  tests  vignettes
$ head TAGS
/home/sfchaos/Documents/applitest/R/data.table/data.table/R/IDateTime.R,775
aas.IDate6,206
aas.IDate.default8,258
aas.IDate.Date11,326
aas.IDate.IDate15,423
aas.Date.IDate17,461
mmean.IDate21,544
rround.IDate37,1028
aas.ITime51,1646

上記の"R CMD tags"コマンドではRのファイルはRディレクトリの直下に存在するものだけしか対象になりません.このような問題点を解決するための手段として,本書ではrtags関数を用いる方法についても説明されています.