渋谷駅前で働くデータサイエンティストのブログ

元祖「六本木で働くデータサイエンティスト」です / 道玄坂→銀座→東京→六本木→渋谷駅前

今更ながら自分でRパッケージを作ってみた(RStan連携も含めて)

f:id:TJO:20210331153629p:plain

元はと言えばアホなエイプリルフールネタを作るために勉強し始めたことなのですが、折角だしということで毎日15時過ぎにやっている「本日の東京都のCOVID-19陽性報告数を踏まえた感染拡大状況把握のためのフィッティング」ネタをRパッケージにまとめて簡単に出来るようにしたのでした。が、そのプロセスが結構落とし穴が多くて大変だったので、後々の自分のための備忘録として書き残しておくことにします。

Rパッケージを作ってみようと思ったきっかけ


元々は恒例のスベるエイプリルフールネタを何にするか考えていて、「ここしばらくニセ書籍表紙ネタが続いていたから久しぶりにGitHubネタにでもするか」と思ったのがきっかけです。その際「できれば実行してみて初めてジョークだと分かる形にしようかな」と考えたもので、すぐ連想したのが「ジョークRパッケージ」でした。これ自体はネタをすぐに思いついたので大した話ではなかったのですが、「そう言えば」と思ったネタが別にあったのです。


そう、ここ半年ぐらい毎日回しているコロナ速報分析です。その中身は上記のRスクリプトで、基本的にはその日の日付と15時に発表される日次の陽性報告数を入力すれば、後は自動的に東京都のサイトから残りのデータをダウンロードしてきて整形し、出来上がったデータセットに対してRStanが回ってフィッテイングした結果が得られるという代物です。


で、上記のジョークパッケージを漫然と作りながらふと「同じことをこのコロナ速報分析のスクリプトにやればパッケージ化できるのでは?」と思ったのでした。そこで、全体のスクリプトを複数の関数に分けてステップごとに適当なオブジェクトを返すようにした上で、15時の発表値だけ入力すれば一貫したプロセスとして回せるようなものを書くところまで来て、いよいよ着手してみたという次第です。


基本的には{devtools}, {usethis}, {roxygen2}があれば万事OK


本来ならば「神」ことHadleyの『Rパッケージ開発入門』辺りを読んで勉強するべきだったのでしょうが、エイプリルフールネタを仕込んでいた時はそんな余裕はなかったので、色々ググってみて見つかった資料を参考にしていました。例えばこの辺です。


流石はTokyo.R界隈の猛者の方々の資料で、エイプリルフールネタに関して言えばこれらを拝読して覚えたままになぞるだけでRパッケージが出来上がりました。詳細はリンク先を是非お読みいただきたいのですが、基本的には以下の通りにやれば出来上がります。

sandbox的なディレクトリに関数を記述したRファイル(.R)を一旦置いておく

これは予め書いておいたRスクリプトをパッケージディレクトリにコピーできるようにしておくというだけの話です。分かりやすいところであれば何でも良いと思います。

{usethis}でパッケージのガワを作る

usethis::create_package("hogehoge")

f:id:TJO:20210408003033p:plain

多分これでhogehogeディレクトリと、Rディレクトリや関連設定ファイル群を含むガワが出来上がって、その上RStudioから実行していれば新たなセッションがR開発プロジェクト(.Rproj)上で立ち上がるはずです。大体のものは後からでも幾らでも直せますが、NAMESPACEだけは"Generated by roxygen2: do not edit by hand"(手書きで編集するな)と書いてあるので触らないようにしましょう*1

関数を記述したRファイルをパッケージディレクトリにコピーし、DESCRIPTIONを適切に書き換える

基本的にはRファイルをRディレクトリにコピーすればOKです。ある程度汎用性を持たせるために色々書き換える必要はありますが、その辺は通常のソフトウェア開発と全く同じなので詳細は割愛します*2


実は結構困ったのが{dplyr}のパイプ演算子。{caret}とかのソースコードを参考にして後述の@importFromとか駆使したりしたんですが、何をどうあがいても%>%が使えなくて非常に困ったので、窮余の策としてベタ書きすることにしました。多分もっと良い方法があるはずなのですが……。


あとはDESCRIPTION。基本的にはパッケージ説明(用途・概要・開発者情報)を書く必要があります。リンク先にも書いてある通り「いい加減に書くと動かない」のは本当で、例えば「著者メアド」をメアドではないように記述する(例えばただのwebサイトURLとか)と途端にインストールできなくなります(笑)。

Roxygen Skeltonを使って各関数に必須の情報を書き加える

usethis::use_roxygen_md()
usethis::use_package_doc()

をやった方が良いかどうかは忘れましたが、多分やった方が良いです(適当)。RStudioだと各関数の宣言のところにカーソルを合わせてからCode -> Insert Roxygen Skeltonとやると、Rヘルプを{roxygen2}で自動生成するために必須の情報を入力するためのroxygenコメントのガワが追記されます。適宜資料を参考にしながら書き加えましょう。


大体のものは書き忘れていても致命的ではありませんが、@exportだけは書き忘れると関数が使えなくなるので要注意。また関数内でlibrary()とかrequire()は使うべきではないとされていて、e1071::svm()のようにするか、@importFrom e1071などで関数の外側から関連パッケージは呼び出すようにします。全部書き終わったら、

devtools::document()

とすればmanディレクトリにヘルプファイルが生成され、NAMESPACEも更新されます。NAMESPACEが期待した通りになっていない場合は、roxygenコメントがきちんと書かれているかどうか確認しましょう。

ライセンス情報を追加する

特段の事情がなければ、

usethis::mit_license()

とやればDESCRIPTION含む設定ファイル群にMITライセンスの表記が追記されます。大体の作業が終わったら、

devtools::check()

として、足りないパッケージなどがないか確認します。roxygenコメントに足りないものがあったりするとここで怒られることがあるので、見つけたら追記しておきましょう。

インストールできるかどうか確認する

パッケージディレクトリが作業ディレクトリになっている場合は、

install.packages("../hogehoge", repos = NULL, type = "source")

とすればソースを読み込んでのインストールが走ります。無事最後まで回ってインストールできれば、準備完了です。

GitHubに上げる

これは僕がいちいち書くまでもないことなので、ググれば幾らでも見つかるGitHubリポジトリの管理方法に関するドキュメントをお読みくださいということで*3。正しくRパッケージとして構築できていれば、普通に

devtools::install_github('repo/hogehoge')

でインストールできるはずです。エイプリルフールのジョークパッケージで実践したのはここまでの内容です。


ただしRStanが関わる場合は{rstantools}を使わなければならない


さて、ジョークパッケージのリリースが上手くいったので、いよいよコロナ速報パッケージでも同じことをしようと思ったのでした。ところが……全然うまくいかない。理由は簡単で、RStanを使う場合はStanファイル(.stan)がモデルのコンパイルのために必要なのですが、これを置く場所が全然分からない。

f:id:TJO:20210408003119p:plain

こんな感じでsrcディレクトリでも作ってそこにStanファイルを置けば動くかなー、と思ったけどやっぱり動かない。これは困ったなぁ……と思って色々ググってみたら、こちらのドキュメントが引っ掛かりました。

そう、{rstantools}です。基本的に利用するユーザーの側がStanモデルを毎回用意するのならそもそもRパッケージにする必要はないというかできない(毎回Stanファイルを自分で書いて用意すれば良い)わけで、ユーザーの側が「予め固定されたStanモデルしか使わない」ことを想定するならば、pre-compiledなモデルをstanmodelクラスのオブジェクトとして用意しなければならないんですね。平たく言えば、コンパイル済みモデルをパッケージインストール時に用意するようにしろ、ということです。


詳細なやり方は上記のvignettesに全て書いてありますが、注意点を以下に記しておきます。なお、前半の{devtools}, {usethis}でやっていた手続きの結構な部分が{rstantools}経由で回せるので、基本的には{rstantools}のvignettesに従えばOKです。

理由がなければcreate_rstan_package()でゼロから作る

usethis::create_package()と同じノリで

create_rstan_package()
use_rstan()

の2種類のパッケージ作成関数が用意されていますが、極めて特段の事情がない限りはcreate_rstan_package()でゼロから作った方が良いです。というのはそもそもRStan連携パッケージは普通のRパッケージと少し構成が違うので、何も調べずに上記のsrcディレクトリにStanファイルを放り込んだだけのものにuse_rstan()しても、全然上手くいきません。

create_rstan_package("hogestan")

とすると、以下のようなパッケージディレクトリが生成されます。

f:id:TJO:20210408003323p:plain

{rstantools}は優秀で、ついでにDESCRIPTIONにも以下のようにImports含めRStan連携に必須のものを全て書き込んでくれます。

Imports: 
    methods,
    Rcpp (>= 0.12.0),
    RcppParallel (>= 5.0.1),
    rstan (>= 2.18.1),
    rstantools (>= 2.1.1)
LinkingTo: 
    BH (>= 1.66.0),
    Rcpp (>= 0.12.0),
    RcppEigen (>= 0.3.3.3.0),
    RcppParallel (>= 5.0.1),
    rstan (>= 2.18.1),
    StanHeaders (>= 2.18.0)
SystemRequirements: GNU make

後は大体上記の標準的なRパッケージ開発の時とやり方は同じです。

RスクリプトとStanファイルを配置する

f:id:TJO:20210408003350p:plain

RスクリプトはRディレクトリの下に置けば大丈夫ですが、Stanファイルはinstの下にstanというディレクトリがあるのでそこに置くことになります。ちなみにsrcディレクトリにはビルドそのものに必要なファイルが既に入っているので、触らないようにしましょう*4


ちなみに、pre-compiledなstanmodelを使う都合上、hoge.stanがあったとしてRの関数側で

fit <- rstan::stan(file = 'hoge.stan', ...)

とStanファイルを読み込む形で書いている場合は、

fit <- rstan::sampling(stanmodels$hoge, ...)

のようにコンパイル済みモデルを呼び出す形に書き換える必要があります。

パッケージの準備が終わったらビルドが必要

Vignettesにも書いてありますが、最後にビルドする必要があります。

example(source) # defines the sourceDir() function
try(roxygen2::roxygenize(load_code = sourceDir), silent = TRUE)
pkgbuild::compile_dll()
roxygen2::roxygenize()

これでビルドされると同時に、ドキュメントも生成され、さらにNAMESPACEが更新されます。後は

install.packages("../hogestan", repos = NULL, type = "source")

として、インストールできることを確認すればおしまいです。全て確認できたら、まるっとGitHubに上げましょう。

ビルド環境が整備されていないとそもそも入らない

……なのですが。僕の手持ち環境の中で一つだけ{TokyoCovidMonitor}が入らなかったものがあるんですが、調べてみたら普通にビルドチェーンがコケていて*5、そもそもパッケージインストール時のビルド自体が回らなかったのでした。当たり前の話ですが、RStan連携パッケージを作る時は事前にビルド環境の動作確認をしておきましょう。


余談


今回は完全に個人用途のしょうもないRパッケージだらけだったので、vignettes/とtests/は用意していません。いずれもっとちゃんとしたRパッケージを書く際には、きちんと入れておこうと思います……。

*1:というか手書きで編集しても後述するdevtools::document()すると全部上書きされて消えます

*2:僕はソフトウェア開発の専門家ではないので、各種専門書をお読みください……

*3:幾星霜ぶりかでGit使ったので結構色々なところでつまずきました……

*4:実は会社のクラウドではsrcにStanファイルを置くので結構戸惑った

*5:多分OSのアップデート時に依存関係が壊れたのではないかと