{renv}と分析環境の管理

作成日

2023年3月20日

更新日

2024年6月27日

はじめに

 分析結果を再生する際に最も重要なのはデータコードだろう。この2つがあれば、多くの場合、分析結果を再生することができる1。しかし、データとコード以外にも再生に欠かせない要素があり、それがパッケージだ。現在において、R内臓の関数のみで構成されたコードは極めて稀であり、外部のパッケージを用いることが多い。今のR界隈において{tidyverse}パッケージ群が必須になっているが、分野によっては他にも必須パッケージがあろう。しかし、これらのパッケージは時間の流れにつれ、徐々に改善されていく。この改善の段階において、既存の関数がなくなったり、関数名が変わったり、推定のアルゴリズムが変わったりするケースもある。この場合、データとコードだけでは再生ところか、エラーが発生してしまうかも知れない。したがって、分析を再生するためには分析時の環境そのものも同じにすることが望ましい。本章では{renv}による分析環境の管理/共有について紹介する。

分析環境と再生可能な研究

 {renv}はreproducible environments(再生可能な分析環境)の略で、現時点での分析環境を保存し、再生してくれるパッケージだ。なぜ分析環境の再生可能性が重要だろうか。いくつかの例を紹介しよう。

 たとえば、古いパッケージではできなかったものが、今はできるようになっている可能性がある。たとえば、{ggplot2}のgeom_pointrange()は点(点推定値)と線(区間)を表現する幾何オブジェクトであるが、昔は点と垂直線のみが引けた。つまり、マッピングは常にxyyminymaxに対して行う必要があった。この区間を垂直線でなく、水平線にしたい場合は、coord_flip()で座標系を90度回転する必要があった。しかし、今の{ggplot2}は、xminxmaxにもマッピングができるため、coord_flip()が不要となる。むろん、昔の書き方もまだ使えるのでこのケースは分析の再現という観点からはあまり問題にならないだろう。

 問題は昔はできたものの、今はできなくなっているケースだ。たとえば、{dplyr}のrowwise()関数は、現在は華麗に復活したものの、実は無くなる予定の関数だった。また、{tidyr}のgather()spread()関数は昔のコードではよく見るが、近い将来、無くなる予定である(現在はpivot_longer()pivot_wider()が使われている)。

 また、関数そのものは残っていても、仕様が変わることによって仮引数名、実引数の使用可能なクラスが異なる場合もある。これは開発途上のパッケージでよく見る現象だ。たとえば、筆者(宋)が愛用するパッケージの{marginaleffects}では推定したモデルの予測値を計算するpredictions()関数と、限界効果を計算するslopes()関数が用意されている。どの関数も戻り値のクラスはデータフレーム型だ。しかし、predictions()で計算された予測値はpredictedという名の列として表示され、slopes()で計算された限界効果の点推定値はdydxという名の列だった。現在は、どの関数を使っても予測値・限界効果の点推定値はestimateという名で統一されている。単に、計算結果をデータフレームとして出力するだけなら問題ないが(見た目は変わるものの、エラーは吐かない)、計算結果を使用して作図を行う場合は話が変わってくる。昔のコードではpredicteddydx列でマッピングした図が、現在はエラーを出してしまうのだ。

 Rとその生態系は毎日のように更新され、改善されていく。これはRのメリットでもあるが、デメリットでもあり、昔のコード(legacy code)がもはや動かない可能性もある。また、近年、学術の界隈でも再現可能性、再生可能性が重要視されている。今すぐに{renv}をインストールしておく理由としては十分すぎる。

pacman::p_load(renv)

分析環境の保存

 {renv}を使った分析環境の保存/再現はプロジェクト機能の使用を前提としている。プロジェクト機能を使わない場合でも{renv}は使用可能だが、相性が良くない。{renv}の使用と関係なく、プロジェクトは非常に便利な機能なので常に使用するように心がけよう。 プロジェクト機能の詳細は『私たちのR』の「基本的な操作」を参照を参照されたい。

 プロジェクトを開いた状態(RStudioの右上に「Project: (none)」と表示されたらプロジェクト未使用中)で、現在の分析環境を保存する方法から紹介する。以下は架空の例であるが、renv_testという名のプロジェクトにmy_script.Rというファイルが存在し、ファイルの中身は以下の通りであるとする。

my_script.R
pacman::p_load(ggdag)

 単に{ggdag}を読み込むだけのスクリプトファイルである。この{ggdag}のバージョンを確認してみよう。

packageVersion("ggdag")
[1] "0.2.7"

 現在の{ggdag}のバージョンは0.2.7である。実は{ggdag}0.2.7はやや古いバージョンである。現時点での分析環境を保存するためにはinit()関数を使う。{renv}の関数群は使う機会が滅多にないので、コンソール上でrenv::init()と入力しよう。

renv::init()
* Discovering package dependencies ... Done!
* Linking packages into the project library ... [60/60] Done!
The following package(s) will be updated in the lockfile:

# CRAN ===============================
- MASS            [* -> 7.3-58.3]
- Matrix          [* -> 1.5-3]
- R6              [* -> 2.5.1]
- RColorBrewer    [* -> 1.1-3]
- Rcpp            [* -> 1.0.10]
- RcppArmadillo   [* -> 0.12.0.1.0]
- RcppEigen       [* -> 0.3.3.9.3]
- V8              [* -> 4.2.2]
- boot            [* -> 1.3-28.1]
- cli             [* -> 3.6.0]
- colorspace      [* -> 2.1-0]
- cpp11           [* -> 0.4.3]
- curl            [* -> 5.0.0]
- dagitty         [* -> 0.3-1]
- digest          [* -> 0.6.31]
- dplyr           [* -> 1.1.0]
- fansi           [* -> 1.0.4]
- farver          [* -> 2.1.1]
- forcats         [* -> 1.0.0]
- generics        [* -> 0.1.3]
- ggdag           [* -> 0.2.7]
- ggforce         [* -> 0.4.1]
- ggplot2         [* -> 3.4.1]
- ggraph          [* -> 2.1.0]
- ggrepel         [* -> 0.9.3]
- glue            [* -> 1.6.2]
- graphlayouts    [* -> 0.8.4]
- gridExtra       [* -> 2.3]
- gtable          [* -> 0.3.1]
- igraph          [* -> 1.4.1]
- isoband         [* -> 0.2.7]
- jsonlite        [* -> 1.8.4]
- labeling        [* -> 0.4.2]
- lattice         [* -> 0.20-45]
- lifecycle       [* -> 1.0.3]
- magrittr        [* -> 2.0.3]
- mgcv            [* -> 1.8-42]
- munsell         [* -> 0.5.0]
- nlme            [* -> 3.1-162]
- pacman          [* -> 0.5.1]
- pillar          [* -> 1.8.1]
- pkgconfig       [* -> 2.0.3]
- polyclip        [* -> 1.10-4]
- purrr           [* -> 1.0.1]
- remotes         [* -> 2.4.2]
- renv            [* -> 0.17.1]
- rlang           [* -> 1.1.0]
- scales          [* -> 1.2.1]
- stringi         [* -> 1.7.12]
- stringr         [* -> 1.5.0]
- systemfonts     [* -> 1.0.4]
- tibble          [* -> 3.2.0]
- tidygraph       [* -> 1.2.3]
- tidyr           [* -> 1.3.0]
- tidyselect      [* -> 1.2.0]
- tweenr          [* -> 2.0.2]
- utf8            [* -> 1.2.3]
- vctrs           [* -> 0.6.0]
- viridis         [* -> 0.6.2]
- viridisLite     [* -> 0.4.1]
- withr           [* -> 2.5.0]

The version of R recorded in the lockfile will be updated:
- R               [* -> 4.2.2]

* Lockfile written to '~/r_projects/renv_test/renv.lock'.

Restarting R session...

* Project '~/r_projects/renv_test' loaded. [renv 0.17.1]

 {ggdag}だけでなく、{ggdag}が依存するパッケージの現時点でのバージョンが固定される。実際、{ggdag}の行を見るとバージョンが0.2.7になっている。このrenv_testプロジェクトがどのパッケージを使用するかは{renv}パッケージが自動的に判断してくれる。具体的にはプロジェクトフォルダー内に存在する全ての.R.Rmd.qmdファイルをスキャンし、読み込まれているパッケージを抽出する仕組みである2。これらのパッケージ情報はプロジェクトフォルダーに別途保存される。dir()関数を使用し、プロジェクトフォルダーの内部を覗いてみよう。

dir()
"my_script.R"   "renv"  "renv_test.Rproj"   "renv.lock"

 普通ならrenv_test.Rprojmy_script.Rのみ存在するはずだが、renv.lockファイルとrenvという名のフォルダーが生成されている。このrenv.lockにはパッケージのバージョンおよび依存関係の情報が書かれている(開いてみると分かる)。また、renvフォルダーにはそのパッケージがまるごと入っている。

分析環境の再生

 init()で固定された分析環境情報を再生するためにはどうすればいいだろう。それを解説する前に、プロジェクトを立ち上げていない状態で{ggdag}をアップデートしてみよう。

install.packages("ggdag")

 続いて、{ggdag}のバージョンを確認してみる。

packageVersion("ggdag")
[1] "0.2.8"

 {ggdag}のバージョンが0.2.8になっている。ここで、RStudioを終了し、先ほど作成したrenv_testプロジェクトをもう一度開いてみよう。最初にRのバージョン情報などのメッセージが表示されるが、ここに普段見ることのない1行が追加されている。

* Project '~/r_projects/renv_test' loaded. [renv 0.17.1]

 これは現在、{renv}で保存された分析環境が再生されていることを意味する。実際、0.2.8にアップデートしたはずの{ggdag}のバージョンを確認してみよう。

packageVersion("ggdag")
[1] "0.2.7"

 最初に保存した0.2.7のままになっている。むろん、{renv}を使わない別のプロジェクトを開けば{ggdag}は0.2.8になっている。RStudio + プロジェクト機能を使う場合、プロジェクトフォルダ―にrenv.lockファイルが存在すると、その情報を読み込んで分析環境を再現してくれる。RStudioさまさまだ。宗教的信念によりRStudioを使わない場合は、コンソールで直接renv::restore()と入力すれば分析環境が再生される。

分析環境の更新

 {renv}を使用するプロジェクトでの作業中、パッケージの追加(install.packages())、更新(install.packages()update.packages())、削除(remove.packages())を行った場合、renv.lockファイルとrenvフォルダーの中身もそれに応じて更新する必要がある。これはコンソール上でrenv::snapshot()を打つだけで良い。

 たとえば、先ほど分析環境を保存した際、{gtable}のバージョンは0.3.1であった。これも若干古いバージョンであるため、install.packages()を使って最新バージョンに更新してみよう。

install.packages("gtable")
Retrieving 'https://cran.ism.ac.jp/bin/macosx/contrib/4.2/gtable_0.3.2.tgz' ...
    OK [downloaded 211.1 Kb in 0.15 seconds]
Installing gtable [0.3.2] ...
    OK [installed binary in 0.4 seconds]
Moving gtable [0.3.2] into the cache ...
    OK [moved to cache in 3.2 milliseconds]
* Installed 1 package in 1.7 seconds.

 これで更新は終わりだ。{renv}で再生された分析環境を使用する場合、パッケージのインストール/更新の画面も普段とやや異なるが、気にする必要はない。とにかく{gtable}のバージョンを確認してみると、0.3.2に更新されていることが分かる。

packageVersion("gtable")
[1] "0.3.2"

 これを保存された分析環境に保存してみよう。分析環境を更新するか否かを尋ねてくるが、yを入力すると更新される。

renv::snapshot()
The following package(s) will be updated in the lockfile:

# CRAN ===============================
- gtable   [0.3.1 -> 0.3.2]

Do you want to proceed? [y/N]: y
* Lockfile written to '~/r_projects/renv_test/renv.lock'.

 現在のプロジェクトを終了し、もう一度renv_testプロジェクトを開いて見ると、{gtable}のバージョンが0.3.2になっていることが確認できよう。

より詳しく知るために

 {renv}の詳細は{renv}開発者のページを参照されたい。

  1. ただし、乱数を発生させる場合はシード(seed)を固定する必要がある。多くの場合はコード内にシード固定の関数が(sed.seed())含まれている。乱数発生の関数が含まれているにも関わらず、シード固定の関数がコード内に含まれていない場合、100%再生することはできないもののほぼ同じ結果が再生できる。↩︎

  2. パッケージの読み込みは通常のlibrary()require()関数以外にも、{pacman}のp_load()にも対応する。↩︎