こわくないRパッケージ開発!2016

  • 53
    いいね
  • 0
    コメント

この記事について

この記事では、R言語で書かれたコードを、パッケージとして開発・管理するメリットとその方法について紹介しています。以下はあくまで概要であるため、詳細についてはぜひ今年2月にオライリージャパンから邦訳刊行された『Rパッケージ開発入門』 や、その原著のR packages(ウェブ版)、そして記事末尾のリファレンスをご参照下さい。

想定する読者層

  • 業務・研究、あるいはプライベートでR言語を使いプログラムを書いている、またはこれから書く予定がある
  • 他の人からもらったRのコード(または、他の人に渡したRのコード)がなぜかうまく動かなかった経験がある
  • 以前書いたソースコードやファイルが散らばっており途方に暮れたことがある
  • Rのパッケージ開発なにそれこわいと感じている

提案

次のような場合、分析タスクの運用や共有を楽にするため、Rのコードをパッケージ化しましょう。

  1. その場限りの分析用ではなく、同じコードを今後も使用する見込があること
  2. 同じコードを複数人が利用すること

なお、「パッケージ化できた」とは、後述するdevtools::check()でERROR、WARNINGともに出なくなっているような状態とします。加えて、特に他の人が触る可能性の高いコードの場合、{testthat}パッケージを用いて、テストコードを書くことを推奨します。

なぜパッケージとしてつくるのか

Rのコードをパッケージの形で管理すると、次のような利点があります。

  1. 他の人へのコードの共有が簡単になる(コマンド1つでインストールされる!)
  2. 実行結果が、環境に依存せず再現する(reproducible)ことが担保しやすくなる
  3. 規約に従うことで、本質的な実装に集中しやすくなる

Rのパッケージは上記 1, 2 の利点を実現するために、「テンプレートやさまざまな規約(convention)」を採用しています。一見きゅうくつに思えるかもしれませんが、こうした実装上の決め事に従うことにより、開発者はファイルの適切な配置に迷う必要がなくなり、本質的な処理の実装に集中しやすくなるのです。

とはいえ、「共有が簡単になる」「再現性が担保できる」などと言われても、すぐにはピンと来ないかもしれません。関数化したり、バージョン管理さえしてれば充分じゃないの? と思うかもしれません。
以下では、あるケースを題材に、コード共有時にひそむ落とし穴について説明します。

ケーススタディ (前編)

あなたは、あるWeb系の会社に入社した新卒ぴかぴかのプログラマです。
どうやらこの会社では、四半期ごとに業務時間の10%を自分の好きなプロダクトの開発に充てることができるようです。
「せっかくそんな制度があるなら、自分もなにか作ってみたい!」 と、あなたは思いました。

1. Rのコードを書く

「しかしちょっと待てよ、業務時間の10%って、正確には何日くらいなんだ? 」
そう思ったあなたは、ひとまずこの4月から6月までの間の、祝日と土日を除いた営業日の数を計算するスクリプトを、Rを使って書いてみることにしました。

日付を入れると曜日を返してくれるwday関数
> lubridate::wday("2016-06-17",label = TRUE)
[1] Fri
Levels: Sun < Mon < Tues < Wed < Thurs < Fri < Sat
西暦を入れるとその年の日本の祝日を返してくれるjholiday関数
> Nippon::jholiday(2016, holiday.names = FALSE)
 [1] "2016-01-01" "2016-01-11" "2016-02-11" "2016-03-20" "2016-03-21" "2016-04-29"
 [7] "2016-05-03" "2016-05-04" "2016-05-05" "2016-07-18" "2016-09-19" "2016-09-22"
[13] "2016-10-10" "2016-11-03" "2016-11-23" "2016-12-23"

これらの処理を使って、コードは以下のようになりました。

祝日と土日を除いた営業日数の計算
library(dplyr) 
library(lubridate)
library(Nippon) 

# 4月~6月の日付が入った配列を作成
date_array <- seq(from = as.Date("2016-04-01"), to = as.Date("2016-06-30"), by = 1)

# 上で作った配列を用い、日付と曜日の2列からなるデータフレームに変換
data.frame(target_date = date_array,
           day_of_week = lubridate::wday(date_array, label = TRUE) ) %>%
  # 祝日と土日を除く処理
  dplyr::filter(! target_date %in% Nippon::jholiday(2016, holiday.names = FALSE),
                ! day_of_week %in% c("Sat","Sun") ) %>%
  # データフレームの行数取得
  nrow()
> 61

なるほど、今年度の第一四半期で業務時間の10%を細かく見積もると、自由な開発に充てられるのは6.1営業日なんだな。そこまでできて満足そうにしていたあなたに、マーケターの先輩が話しかけてきます。
「ねぇ、これ何計算してたの?」
「営業日の数を数えるプログラム書いてたんですよ」
「面白そうじゃん、R入ってればそれ動くの?」
「動きますよ、あ、依存パッケージがいくつかあるので、エラーがでたらそれ追加でインストールすれば」
「あーなるほどね、なんかinstall.packages("ほにゃらら")ってすればいいんだよね」

あなたは、いま書いたコードを先輩に使ってもらうために、関数にして渡してあげることにしました。

2. 関数にしてみる

対象の期間の日付データを作る関数と、土日や休日を除いてカウントする関数の2つに分けました。
(本来はもっと細かくするべきかもしれませんが、説明のため2つにしています)

count_busuiness_days.R
library(dplyr)
library(lubridate)
library(Nippon)

seq_days_in_months <- function(year, from_month, to_month){
  # 何ヶ月分の日付の配列を作るか
  month_length <- to_month - from_month + 1
  # 日付の連番の1日目を作成
  start_date <- as.Date(paste(year, from_month, "01", sep="-"))
  # 日付の連番の最後の日を決定
  end_date <- start_date %m+% months(month_length) - 1
  dates <- seq(from = start_date, to = end_date, by = 1)
  return(dates)
}

count_business_days <- function(year, from_month, to_month){
  date_array <- seq_days_in_months(year, from_month, to_month)
  data.frame(target_date = date_array,
             day_of_week = lubridate::wday(date_array, label = TRUE) ) %>%
    dplyr::filter(! target_date %in% Nippon::jholiday(year, holiday.names = FALSE),
                  ! day_of_week %in% c("Sat","Sun") ) %>%
    nrow()
}

上のコードをcount_business_days.Rという名前で保存すると、以下のように2行でプログラムが実行できます。

> source("count_business_days.R")
> count_business_days(2016, from_month = 4, to_month = 6)
[1] 61

関数化したので、例えば対象の期間を第二四半期(7月〜9月)としても、以下のようにオプションを変えるだけで答えが得られます。

> count_business_days(2016, from_month = 7, to_month = 9)
[1] 62

めでたしめでたし。
ところが、このプログラムには思わぬ落とし穴が潜んでいました。

3. 再現性は担保されているか

先輩社員にさきほどのコードを渡したところ、2016年7月~9月の平日の日数を計算すると、実際とは異なる答えが返ってきたというのです。

> count_business_days(2016, from_month = 7, to_month = 9)
[1] 63

「困るよ...平日と休日だと広告費の使い方ぜんぜん違うから、これじゃ安心して使えないよ...」
「え、僕の環境だとちゃんと62って計算できてますよ?」

先輩の実行環境
 Nippon     * 0.5.3   2013-07-26 CRAN (R 3.2.3) # devtools::session_info() の結果
あなたの実行環境
 Nippon    * 0.6.3-1  2016-04-13 CRAN (R 3.3.0)

実は、今年から新しく山の日という祝日が増えています。この祝日を増やす法律が可決成立されたのは2014年5月23日。2013年にリリースされた0.5.3の時点ではまだ「山の日(8/11)」の追加は反映されておらず、先輩の実行環境では実際より平日がひとつ多く計算されてしまったのです。

library()関数は、バージョンを指定すること無く、実行環境にあるパッケージをそのまま使ってしまいます。

さて、さきほどのプログラムの最大の問題は、実行環境によって結果が異なる可能性を考慮していないことです。
どうすればよかったのでしょうか?

データを提供するパッケージに限らず、出力結果が変わることは充分起こり得ます。パッケージ化して依存パッケージのバージョンを記述することで、より再現性を担保することができるのです。

※より厳密に、パッケージの依存関係を記録・再利用できるpackratという仕組みも存在しますが、ここでは詳解しません。Packratの使いみちを考えてみた をぜひ参照してください。

ケーススタディ (後編)

0. 前準備

  • 比較的最新のR実行環境
    • R (>= 3.1.2) ※現在の最新の安定版は3.3.1です
    • RStudio (>= 0.99.902)
  • いくつかの拡張パッケージ
    • devtools (>= 1.11.1)
    • roxygen2 (>= 5.0.0)
    • testthat (>= 0.7)
  • 開発用の環境
    • Windowsの場合: Rtools / Macの場合: Xcodes / Linuxの場合: r-devel
    • Git

R本体やRStudio、各パッケージのバージョンはdevtools::session_info()で確認することができます。Rtools等が入っているかどうかは、devtools::had_devel()を実行すると確認することができます。入っていない場合、もしWindowsであればInstall Rtools for Windowsを参考にRtoolsを別途インストールしてください。この際、リンク先にもあるようにシステム環境変数を手動で書き換えるのではなく、ダイアログ中に出てくる"edit the system path" のチェックボックスを入れることを強く推奨します。
また、RStudioでのGit利用については、RStudioではじめるGitによるバージョン管理 をぜひ参照して下さい。

作業ディレクトリ(working directory)の設定
以下の作業は、新しいディレクトリで行うことをおすすめします。なお、デフォルトでは作業ディレクトリ名がそのままパッケージ名になります。例として、ここではexamplerという名前にしてみましょう。
RStudioであれば、Ctrl + Shift+ H もしくはSession→Set working directory→choose directoryでパスを打ち込まなくてもsetwd()関数で作業ディレクトリが設定できます。

※説明のため、RStudioのNew Project...から辿れる"Create a new R package"機能はあえて使いません。この機能は便利なのですが、パッケージ作成の場合は、DESCRIPTIONなどのテンプレートが{devtools}パッケージの最新テンプレートと違うことがあるので注意してください。

1. パッケージのひな形をコピーする

{devtools}パッケージにはパッケージのテンプレート(ひな形)が同梱されています。これをコピーするために、作業ディレクトリでdevtools::create(".")もしくはdevtools::setup()を実行してください。すると、以下の6つのコンポーネントが生成されます。

  • R/
  • DESCRIPTION
  • NAMESPACE
  • example.Rproj
  • .Rbuildignore
  • .gitignore

この中で、後から編集するのは 上で太字にしたDESCRIPTIONとR/ディレクトリおよび.gitignoreだけで大丈夫です。その中でも、本稿ではR/ディレクトリとDESCRIPTIONのみに手を加えてミニマムなパッケージを作成します。
各ファイルの役割は後述します。

createとsetupの違いは、作業ディレクトリにファイルやフォルダがあるかどうかです。作業ディレクトリが空でない場合はcreate()だとエラーが出るので、devtools::setup()を実行してください。

2. roxygenコメントを書く

さきほど書いたcount_business_days.R中のコメントをroxygenコメント形式に書き直し、R/ディレクトリ以下に保存します。roxygenコメントは、

  • コメントの行頭が#ではなく #'で始まっている
  • @paramなどの タグを用いコメントを構造化する

という特徴があります。コメントを定形に則って書くことで、{roxygen}パッケージがこれらを解釈し、関数をはじめとするRのオブジェクトについて

  • マニュアル(help()関数で呼び出せるヘルプ)
  • NAMESPACE

のファイルを自動で更新してくれます。

R/count_business_days.R
# Generate Sequence of Dates
#' @importFrom lubridate %m+%
seq_days_in_months <- function(year, from_month, to_month){
  month_length <- to_month - from_month + 1
  start_date <- as.Date(paste(year, from_month, "01", sep="-"))
  end_date <- start_date %m+% months(month_length) - 1
  dates <- seq(from = start_date, to = end_date, by = 1)
  return(dates)
}

#' Calculating the Number of Business Days
#' 
#' @param year target year
#' @param from_month start of the range
#' @param to_month end of the range
#'
#' @importFrom dplyr filter
#' @importFrom dplyr %>%
#' @importFrom lubridate wday
#' @importFrom Nippon jholiday
#' @export
#'
count_business_days <- function(year, from_month, to_month){
  date_array <- seq_days_in_months(year, from_month, to_month)
  data.frame(target_date = date_array,
             day_of_week = lubridate::wday(date_array, label = TRUE) ) %>%
    dplyr::filter(! target_date %in% Nippon::jholiday(year, holiday.names = FALSE),
                  ! day_of_week %in% c("Sat","Sun") ) %>%
    nrow()
}

roxygenコメントを正しく機能させるため、パッケージ利用時にユーザの目に触れる関数については、最低限以下の4項目が含まれているか確認してください。

  • 1行目
    • roxygenコメントの1行目に、そのオブジェクトがどのような役割を果たすかを1行で記述して下さい。この記述をもとに、ヘルプページでのタイトルが生成されます。Descriptionにも同じ文言が入っていますが、これを別の記述にしたい場合は@descriptionタグを用いてオブジェクトの詳細を記載して下さい。
  • @importFrom [package] [func]
    • 関数内で外部パッケージの関数を使用している場合、library()関数の代わりにこのimportFromタグを使って明示してください。特定の関数でなくパッケージ全体を使えるようにする@importを用いることもできますが、そうすると関数名が衝突する可能性が高くなります。可能な限り@importFromを用いるようにしましょう。
  • @param [param] [description]
    • 関数であれば、この@paramタグを使って引数の名前とその説明を記述して下さい。この情報を元に、ヘルプページでUsageとArgumentsの記述が生成されます。
  • @export
    • 上の2つの関数のうち、seq_days_in_months()関数はユーザが実行できる必要はありません。一方、count_business_days()関数は、実際にユーザが実行できなければいけません。これを区別するため、後者に@exportタグを付けます。

その他、知っておくと良いタグは@example@return@inheritParamsなど様々ありますが、詳細についてはぜひ『Rパッケージ開発入門』の5章もしくはObject documentation - R packagesを参照してください。

roxygenコメントを記載し終えたら、以下のコマンドを入力すると、man/ディレクトリ以下にドキュメントファイルが生成されるので、help()関数を使って、マニュアルが参照できるようになっています。

> devtools::document()
Updating exampler documentation
Loading exampler
Writing NAMESPACE
Writing seq_days_in_months.Rd
> devtools::load_all()
Loading exampler
> help(count_business_days)
Using development documentation for count_business_days

help.PNG

そして、NAMESPACEファイルの中身は以下のようになっているはずです。

NAMESPACE
# Generated by roxygen2: do not edit by hand

export(count_business_days)
importFrom(Nippon,jholiday)
importFrom(dplyr,"%>%")
importFrom(dplyr,filter)
importFrom(lubridate,"%m+%")
importFrom(lubridate,wday)

NAMESPACEには依存関係が反映されているものの、DESCRIPTIONにはまだパッケージの依存関係が反映されていません。このままですと、次の項で説明するdevtools::check()を実行した時に、
Namespace dependencies not required: ‘Nippon’ ‘dplyr’ ‘lubridate’ というエラーが出てしまうため、次に進む前に以下のコマンドを実行してください。

devtools::use_package("Nippon")
devtools::use_package("lubridate")
devtools::use_package("dplyr")

すると、次のようなDESCRIPTIONファイルが得られます。

DESCRIPTION
Package: exampler
Title: What the Package Does (one line, title case)
Version: 0.0.0.9000
Authors@R: person("First", "Last", email = "first.last@example.com", role = c("aut", "cre"))
Description: What the package does (one paragraph).
Depends:
    R (>= 3.3.0)
License: What license is it under?
Encoding: UTF-8
LazyData: true
RoxygenNote: 5.0.1
Imports: Nippon,
    lubridate,
    dplyr

Nipponパッケージのバージョンを指定するため、12行めを次のように書き換えて次に進みましょう。

Imports: Nippon (>= 0.6.3),

3. 開発のプロセス

パッケージの中身をロードする

Rパッケージのライフサイクルには次の5つの状態があります。

  1. ソースパッケージ(Source)
    • R/ やDESCRIPTIONなどのコンポーネントを含む単なるディレクトリ
  2. バンドルパッケージ(Bundle)
    • 上記のディレクトリが一つのファイル(.tar.gzなど)に圧縮されたもの
  3. バイナリパッケージ(Binary)
    • Rパッケージ開発ツールを持っていないRユーザにパッケージを配布する場合に、作る必要があるもの。
  4. インストール済のパッケージ(Installed)
    • 単にバイナリパッケージがパッケージライブラリに解凍されたもの。
  5. インメモリパッケージ(In memory)
    • マシンのメモリにロードされて、使える状態になっているパッケージ。

通常、われわれはlibrary()関数を使い、インストール済みのパッケージをメモリにロード(4→5)して使っていますが、パッケージ開発の際はdevtools::load_all()でソースパッケージからメモリにロード(1→5)して動作を確認しながら開発します。

> devtools::load_all()
Loading exampler
> exampler::count_business_days(2016, 7, 9)
[1] 62

パッケージを読み込んで、関数が使えるようになりました!

パッケージの中身をチェックする

devtools::check()を実行すると、パッケージが種々の規約に則っているかのチェックが行われます。(一度check()を実行したのちRStudioを再起動すると右上にBuildタブが現れ、Ctrl + Shift + Eでもcheck()がかけられるようになります。)

> devtools::check()

ERROR, WARNINGが出ない状態を目指す

ここまでのサンプルコードを実行してみると、devtools::check()実行時に以下のようなWARNINGが出るはずです。パッケージ配布時の目安ですが、ERROR, WARNINGは最低限解消しておきましょう。

R CMD check results
0 errors | 1 warning  | 1 notes
checking DESCRIPTION meta-information ... WARNING
Non-standard license specification:
  What license is it under?
Standardizable: FALSE

上の例ですと、devtools::use_mit_license()関数を使ってライセンスファイルを生成させれば、WARNINGを解消することが出来ます。また、他に適切なライセンスがある場合はそれに書き換えましょう。

before
License: What license is it under?

実行前後で、DESCRIPTIONファイルの該当行が下のように書き換わり、devtools::check()が通るようになります。

after
License: MIT + file LICENSE
R CMD check results
0 errors | 0 warnings | 1 notes

R CMD check succeeded

※なお、上記で出ているNOTEは、globalVariables()という関数を使うと解消できるのですが、hadleyにhideous hackとまで言われてしまっているのでここでは積極的には紹介しないことにします。作ったパッケージをもしCRANに投稿したい場合は、こうしたNOTEも丹念にひとつひとつ潰していくことが求められます。

4. コードの質を上げる

Rパッケージを開発する際は、実装者以外によるコードレビューを行うことを推奨します。とはいえ、それが難しい場合も多いはずなので、自力でコードの質を上げるための方法をひとつ紹介します。

  • lintrパッケージ
    • lintr::lint_package()を実行するとR/ディレクトリ内のコードに対して文法チェックを行ってくれる

{formatR}というパッケージもありますが、こちらは直接コードを書き換えてしまうため、あまりおすすめできません。
formatR/lintrについては、Rでコーディングスタイルを適用させる方法 - INPUTしたらOUTPUT!をぜひ参照してください。

5. 公開&共有

パッケージ化の醍醐味は、自分の作ったパッケージを他のユーザーにシームレスに共有できた時ではないでしょうか。githubで公開するとdevtools::install_github()で他の人にインストールしてもらえます!

devtools::install_github("wakuteka/exampler")
...
* installing *source* package 'exampler' ...
** R
** preparing package for lazy loading
** help
*** installing help indices
** building package indices
** testing if installed package can be loaded
* DONE (exampler)
> library(exampler)
> count_business_days(2016, 7, 9)
[1] 62

通常のパッケージのようにインストールして、関数が使用できました!

github以外でも、gitlabなどを使って社内のみからアクセスできるリポジトリがある場合は、同様の機能を持つdevtools::install_git()という関数を使いましょう。

Git/Githubそのものを使えるようにするためには、連載「こっそり始めるGit/GitHub超入門」も参考にしてみてください。

  • 余談ですが、List of R package on githubによると、現在11,841個ものR packageがgithub上に存在するようです。
  • なおさらに余談ですが、hoxo_mさん作の{githubinstall}という、「githubに存在するRのパッケージを簡単にインストールできるパッケージ」が最近CRANに追加されています。typoにも対応しててちょうべんり。

パッケージ構成要素ごとのコメント

以下、駆け足でパッケージのおもな構成要素についてコメントしていきます。

Code (R/)

パッケージに含める.Rファイルは基本的にこのフォルダに格納することになります。もしパッケージ開発中などに、パッケージには含めないコードをどこかに保存したい場合は、.Rbuildignoreに以下の行を追加して、tmp/フォルダ以下に一時的なファイルを保存するという手もあります。

^tmp

Package metadata (DESCRIPTION)

CRANにないパッケージを自動でインストールできるようにする

Importsに記載しただけでは、CRAN以外にあるパッケージはインストールされません。githubや、社内からのみアクセスできるリポジトリにあるパッケージを依存関係に含めたい場合、Package Remotes (devtools/vignettes)を参考に、DESCRIPTIONファイルのImportsにパッケージ名を記載した上で、Remotes:にURL等を追記しましょう。

Object documentation (man/)

devtools::document()によってroxygenコメントから生成された.Rdファイルが置かれるフォルダです。

Testing (tests/)

testthatパッケージを使うと、パッケージのテストをすることができます。テストコードはこのtest/ディレクトリ以下に置くことになります。「そもそもなぜテストコードを書くのか」という点についてはここでは詳細に触れませんが、ひとつ利点を挙げるなら、関数の中身を少し書き変えたいとき、そのインプット/アウトプットが変わってないことを確信を持ったままリファクタリングすることができます。

Namespaces (NAMESPACE)

このファイルは直接いじらずにdevtools::document()で生成したままにしておくようにしましょう。

Data (data/)

パッケージに、ユーザに利用可能なデータを含めて配布したい場合、devtools::use_data()を使うと.RDataもしくは.rda形式のバイナリファイルとして保存することができます。

なお、ユーザーに見せる必要がない、関数内部でのみ使用するデータはdata/ディレクトリ以下ではなく、R/sysdata.rdaに保存するとよいでしょう。

Installed files (inst/)

csv形式の生データやsqlファイル、pythonスクリプト等をパッケージに含めたい場合、それらのファイルは/inst/配下に置き、読み込むときはsystem.file()関数を用いてフルパスを指定することになります。

Other components

.gitignore

devtools::setupを使うと、以下のファイルが自動で生成されます。
この.Rhistoryや.Rproj.userの記載を消してしまったりすると、コミットログに大量のゴミが紛れ込んでしまいますので避けましょう。

.gitignore
.Rproj.user
.Rhistory
.RData

おわりに

リファレンス(参考文献とURL)

Special Thanks

この記事は、rstudio.comで公開されているPainless package development for R にヒントを得て、株式会社ネクストの社内制度である「クリエイターの日」の成果物のひとつとして作成しました。 

クリエイターの日とは、四半期ごとに最大7日間(業務時間の10%)を使い、個人またはチームで「通常業務の枠を離れて、新たな技術や手法に取り組む」ことができるという制度です。

今四半期は同期の @ninomiyt と2人で社内用Rパッケージを開発するプロジェクトを企画しました。61営業日のうち7日間を使い、5日間でパッケージ開発とドキュメントの整備を行い、残りのべ2日ほどの業務時間を使ってまとめたのがこの記事です。

かつてお世話になった先輩の残してくれたドキュメントに敬意を表しつつ、今期から部署に入った後輩がすこしでも楽しいR生活を送れたら、と思って書きました。

Enjoy!