Docker + R + checkpointで再現可能な解析プロジェクトを作る
1. はじめに
1.1. 統計解析の再現性
近年、科学の広い分野で研究結果の再現性を確保する取り組みが進められています。再現性を確保するための要件はさまざまな角度から議論されており、大雑把に言えば以下の2つが重要です。
- 解析前に、根拠に基づいて研究計画書(プロトコル)を作成し、公開する
- 解析後に、第三者が追試できるような状態を整え、提供する
解析前の段階では、多くの分野でプロトコルを作成し公開することが強く推奨されており、すでに主要なジャーナルはプロトコルの登録番号がないと原稿を受理しません。たとえば、メタアナリシスではいくつかのガイドラインを遵守したうえで、審査を受けたプロトコルをPROSPEROで事前に公開することが、査読に進むための最低条件です。[1]
一方、解析後の段階については状況はより混沌としており、具体的な手段に関する合意は形成されていません。2022年4月現在、いくつかのジャーナルがデータセットと解析コードの公開を原稿受理の条件としたり、専門のレビュワーがデータとコードセットから研究結果を再現できるか確認したりしていますが、こうした取り組みはまだ稀です。とはいえ、今のうちから再現性を確保できるような解析方法を仕組みとして実装しておくことは必要でしょうし、研究の品質向上にも役立つと思います。[2]
1.2. 実装
1.2.1. Rのバージョンを管理し解析環境をコンテナー化する
解析環境をどこまで再現できるようしておくかは悩むところですが、[3] オペレーティングシステム(OS)や統合解析環境(IDE)も解析結果に影響を与えることがありますし、追試者がそこまで統一するのはなかなか困難です。そこでコンテナー技術を活用して、これらも簡単に再現できるようにします。今回はDockerを採用し、Rocker Project が作成している rocker/rstudio
イメージで、OS、IDEおよびRのバージョンを管理します。
1.2.2. Rのパッケージ群を管理する
CRAN Task View: Reproducible Researchに、再現性の確保に役立つパッケージが紹介されており、"Package Reproducibility"の項でさまざまなRのパッケージ管理方法がまとめられています。その中から、今回は論文の執筆に向いた便利な特徴を備えている checkpoint
を選びました。
- CRANに存在するパッケージの特定日のスナップショットインストールすることができる
- 全パッケージのリストを確認しなくても、スナップショット日と、Rのバージョンと主要な解析パッケージ名というわずかな情報から、解析に使用したソフトウェア群を特定できる
- 簡単に複数のスナップショット日を切り替えることができるので、解析パッケージ群の変更によって解析結果が変化したかどうかを検証するのが楽
- 使い方がシンプル
- 解析コードの冒頭に1つの関数を記述しておけば解析パッケージの固定も復元も可能
-
checkpoint
はMicrosoft R Archived Networkからパッケージだけではなく、スナップショット日のドキュメントも保存してくれるので、過去のドキュメントを探しに行かなくてもいい
- 動作が安定している
-
checkpoint
のバージョン1.0以降はそれ以前のバージョンとは別物で、パッケージのキャッシュが利用でき、リストアが早い
-
パッケージの初回ビルドはコンパイルが必要なので時間がかかりますが、2回目以降はキャッシュを読み込むだけなので高速です。初回ビルド中はコーヒーブレイクということで。
1.2.3. 解析データと解析レポートを統合する
先に挙げた "CRAN Task View" の "Literate Programming(文芸的プログラミング)"の項にあるように、解析コード、解析結果および解釈をひとつのドキュメントにまとめて直接解析レポートを生成すると改ざんの余地がなく、研究の作業フローもシンプルになります。そこで、この解析プロジェクトのテンプレートではQuartoまたはR Markdownを使用することを前提にしました。
QuartoとR Markdownは、以下のような特徴を持つドキュメント生成システムです。
- Markdown記法を採用している
- ひとつのファイル(
.qmd
または.Rmd
ファイル)にRやPythonなどのコードと通常の文章を同時に書くことができる - HTML、PDF、ワードファイルなど、さまざまな形式のドキュメントを出力できる
- 出力結果にコードの実行結果を含めることができる
QuartoとR Markdownとの使用感はほぼ同じで、Rユーザーにとっては「Quarto = 次世代版R Markdown」という認識で良いと思います。Quartoはまだ開発版ですが、仕様の変化が減ってきている(気がする)ので、この記事ではQuartoの設定例を紹介します。
1.2.4. データセットと解析コードを公開する
追試者や、将来の研究者の利便性を考えると、データセットと解析コードをリポジトリに公開してデジタルオブジェクト識別子(DOI)を付与し、論文で引用するのがスムーズだと思います。今回はGit/GitHubを使用し、Zenodoと連携することを想定しています。
リポジトリができ上がった後の話なのでこの記事では詳しく書きませんが、GitHubとORCiD/Zenodoは簡単に連携できます。下の画像は、Zenodoの操作説明ですが、このぐらいシンプルです。
1.3. やりたいこと
以下のように解析プロジェクトを構成します。
- DockerでRStudio Serverを動かす
-
rocker/rstudio
イメージを使用する - MakeとDocker Composeで、簡単に起動・終了できるようにする
-
-
checkpoint
でプロジェクト固有のパッケージライブラリを作成・管理する- Rパッケージのキャッシュはホストからマウントする
- QuartoまたはR Markdownが使用できるようにする
- Git/GitHubでソースコードとデータセットをバージョン管理する
1.4. テストした環境
- OS:Windows 10 Pro(バージョン:21H2、OSビルド19044.1586)
- WSL2-Ubuntu 20.04.4 TLS
- gitとmakeが使用可能で、GitHubに接続可能
- WSL2-Ubuntu 20.04.4 TLS
- Docker Desktop 4.6.0(WSL2バックエンド)
- Docker version 20.10.13, build a224086
- Docker Compose version v2.3.3
2. 成果物
2.1. 解析プロジェクトのテンプレートリポジトリ
今回作ったテンプレートリポジトリをGitHubで公開しました。踏み台として使っていただけたら幸いです。
2.2. リポジトリの構成
主な設定ファイルは以下の通りです。
.
├── .Rprofile 【R:セッション開始時に実行するスクリプト】
├── .checkpoint/ 【R:checkpointによるパッケージキャッシュ】
├── .env.example 【Docker Compose:環境変数の設定テンプレート】
├── .git/
├── .gitignore
├── LICENSE
├── Makefile 【Make:起動コマンド短縮】
├── README.md
├── codes/
│ ├── .gitignore
│ ├── README.md
│ ├── analysis.qmd 【Quarto:解析コードと結果の解釈】
│ ├── analysis_cache/
│ ├── analysis_files/
│ ├── datasets/ 【Quarto:データセット】
│ ├── figures/ 【Quarto:プロットの画像データ(.svg)】
│ └── templates/ 【Quarto:引用文献の書誌要素とスタイル定義】
├── compose.yml 【Docker Compose:設定ファイル】
├── dev/
│ ├── docker/
│ │ ├── .Renviron
│ │ ├── .ssh/
│ │ ├── Dockerfile 【Docker:設定ファイル】
│ │ └── startup.sh*
│ └── dotfiles/ 【RStudio:設定ファイル】
└── research.Rproj 【R:Rプロジェクトファイル】
3. 主な設定ファイル
3.1. Makefile
DockerとDocker composeのコマンド群を短縮します。ひとまず最低限の内容としましたが、シェルスクリプトを書いて色々仕込むこともできそうです。
.DEFAULT_GOAL := help
.PHONY: help
help: ## show a command list ## N/A
@echo "Command list:"
@printf "\033[36m%-9s\033[0m %-48s %s\n" "[Target]" "[Description]" "[Product]"
@grep -E '^[/a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | perl -pe 's%^([/a-zA-Z_-]+):.*?(##)%$$1 $$2%' | awk -F " *?## *?" '{printf "\033[36m%-9s\033[0m %-48s %s\n", $$1, $$2, $$3}'
.PHONY: up
up: ## build and run RStudio Server in the background ## docker compose up -d --build
@docker compose up -d --build
@echo "💡 Access: http://localhost:8787/"
.PHONY: down
down: ## stop and remove running containers and networks ## docker compose down
@docker compose down
.PHONY: stop
stop: ## stop running containers without deleting it ## docker compose stop
@docker compose stop
.PHONY: start
start: ## (re-)start stopped containers ## docker compose start
@docker compose start
.PHONY: ps
ps: ## list containers for the compose-project ## docker compose ps
@docker compose ps
.PHONY: prune
prune: ## delete unused Docker objects ## docker system prune -f
@docker system prune -f
.PHONY: bash
bash: ## start bash in the rstudio container ## docker compose exec rstudio /bin/bash
@docker compose exec rstudio /bin/bash
この研究プロジェクトを配置したディレクトリ(Makefileのある場所)で make up
コマンドを実行すると、バックグラウンドでRStudio Serverを起動します。ブラウザからRStudio Serverにアクセスすれば、解析が開始できます。同様に、make down
で終了、make
でコマンドのヘルプを閲覧できます。
【起動】
make up
【実行結果(抜粋)】
[+] Building 0.3s (12/12) FINISHED
[+] Running 1/0
⠿ Container r-project-rstudio-1 Running 0.0s
💡 Access: http://localhost:8787/
【終了】
make down
【実行結果】
[+] Running 2/2
⠿ Container r-project-rstudio-1 Removed 11.8s
⠿ Network r-project_default Removed 0.7s
【ヘルプを開く】
make
【実行結果】
Command list:
[Target] [Description] [Product]
help show a command list N/A
up build and run RStudio Server in the background docker compose up -d --build
down stop and remove running containers and networks docker compose down
stop stop running containers without deleting it docker compose stop
start (re-)start stopped containers docker compose start
ps list containers for the compose-project docker compose ps
prune delete unused Docker objects docker system prune -f
bash start bash in the rstudio container docker compose exec rstudio /bin/bash
3.2. .env.example
環境変数を設定する .env
ファイルのテンプレートです。.env.example
をコピーして .env
ファイルをつくり環境変数を設定すると、その値はCompose fileに記載された既定値より優先されます。.env
は .gitignore
でGitの管理から外されているので、自分のマシン環境に依存した設定内容はリポジトリにアップロードされません。ここではコンテナー内のタイムゾーン、RStudioの一連の設定ファイル(dotfiles)のディレクトリ、コンテナー内のGitからGitHubへ接続するためのSSHキーのパスを指定できます。
# Time zone of the Docker host machine
# Default value is "UTC"
LOCAL_TZ="UTC"
# The path to your universal dotfiles directory
# on the Docker host machine.
# Default value is "./dev/dotfiles"
DOTFILES_ROOT="./dev/dotfiles"
# The path to your SSH private key file to connect GitHub.
# Default value is "/dev/null"
GITHUB_SSH_IDENTITY="/dev/null"
3.2.1. .envの設定例
LOCAL_TZ
の設定
3.2.1.1. rocker/rstudio:4.1.3
のコンテナー内OSはDebian "bullseye" で、時刻系が協定世界時(UTC)になっています。Debian系のディストリビューションの場合は、環境変数 TZ
を Asia/Tokyo
とすることで、日本標準時(JST)に切り替えることができます。.envで LOCAL_TZ
の値を設定し、Compose fileでこの値を環境変数 TZ
に渡します。
- .envの
LOCAL_TZ
にAsia/Tokyo
と記載する
DOTFILES_ROOT
の設定
3.2.1.2. デフォルトでは、RStudio Serverの設定ファイルは各プロジェクトに固有となっています。以下の手順で、他の端末・研究プロジェクトでも設定を統一できます。
- GitHubで研究用Rプロジェクトの
./dev/dotfiles
と同じファイルを含むUserName/dotfiles_rstudio
リポジトリを作成する-
.gitignore
は空ファイルにする
-
-
UserName/dotfiles_rstudio
リポジトリをWSL2-Ubuntuの~/res
ディレクトリにクローンする - .envの
DOTFILES_ROOT
に~/res/dotfiles_rstudio
と記載する
GITHUB_SSH_IDENTITY
の設定
3.2.1.3. コンテナー内からGitHubに接続できるように設定します。前回の記事では、SSH keyは生成しませんでしたので、まずSSH keyを生成します。なお、既存のSSH keyがある場合は上書きされてしまうので注意が必要です。-C
オプションにはGitHubに登録したメールアドレスを入力します。
cd
ssh-keygen -t ed25519 -C "youremail@domain.com"
~/.ssh
ディレクトリが作成され、公開鍵と秘密鍵のペアが生成されるので、.env
ファイルの GITHUB_SSH_IDENTITY
にSSH keyのパスを記載した後、ターミナルソフトで公開鍵を表示・コピペし、公式ドキュメントにしたがってGitHubに登録します。
cat ~/.ssh/id_ed25519.pub
3.2.1.4. コンテナー内からGitHubへ接続
RStudio ServerのTerminalペイン(Consoleではない)で以下のコマンドを実行します。これで、コンテナー内のRStudio ServerからGitHubに接続できます。
git config --global user.name "Your Name"
git config --global user.email "youremail@domain.com"
ここで設定した内容は ~/res/dotfiles_rstudio/.gitconfig
に記録されます。
3.3. Compose file
解析プロジェクト、RStudioの設定ファイルおよびGitHub接続用のSSHキーを、ホストからコンテナーにバインドマウントします。SSHキーはDocker側からの意図せぬ書き換えを防止するため、読み取り専用です。
services:
rstudio:
build: dev/docker
ports:
- "8787:8787"
environment:
- TZ=${LOCAL_TZ:-UTC}
- PASSWORD=password
- DISABLE_AUTH=true
volumes:
- type: bind
source: "."
target: "/home/rstudio/project"
- type: bind
source: "${DOTFILES_ROOT:-./dev/dotfiles}"
target: "/home/rstudio/dotfiles"
- type: bind
source: "${GITHUB_SSH_IDENTITY:-/dev/null}"
target: "/home/rstudio/.ssh/id_github"
read_only: true
3.4. Dockerfile
ベースとなるイメージは rocker/rstudio:4.1.3
です。もし、ggplot2
などで日本語フォントを使用する場合は必要なaptパッケージ(fonts-ipaexfont
など)をインストールします。Rのユーザーライブラリには checkpoint
、pacman
および rmarkdown
をインストールします。
FROM rocker/rstudio:4.1.3
# Install apt packages
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ssh \
libxt-dev \
libxml2-dev \
libgit2-dev \
libfontconfig1-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install R packages
RUN install2.r -e -s -n -1 \
checkpoint \
pacman \
rmarkdown \
&& rm -rf /tmp/downloaded_packages/ /tmp/*.rds
# Add github.com to known_hosts in SSH
RUN mkdir -p /home/rstudio/.ssh \
&& ssh-keyscan -t rsa, ed25519 github.com > /home/rstudio/.ssh/known_hosts \
&& chown -R rstudio:rstudio /home/rstudio \
&& chmod 700 /home/rstudio/.ssh
# Copy the configuration files
COPY .Renviron /home/rstudio/.Renviron
COPY .ssh/config /home/rstudio/.ssh/config
# Copy the startup script
COPY startup.sh /startup
# Change the startup command
CMD [ "/startup" ]
3.5. .Rprofile
各種エラーメッセージを有効化しておきます。ユーザーライブラリにインストールした checkpoint
、pacman
および rmarkdown
はQuartoで呼び出すので、library()
でアタッチしておく必要はありません。
-
warn = 1
警告メッセージが発生したら、すべて表示する -
warnPartialMatchArgs = TRUE
部分一致の使用警告(引数)を有効にする -
warnPartialMatchAttr = TRUE
部分一致の使用警告(属性抽出)を有効にする -
warnPartialMatchDollar = TRUE
部分一致の使用警告(リスト要素)を有効にする -
showWarnCalls = TRUE
警告メッセージの呼び出し履歴の概要を表示する -
showErrorCalls = TRUE
エラーメッセージの呼び出し履歴の概要を表示する
options(
warn = 1,
warnPartialMatchArgs = TRUE,
warnPartialMatchAttr = TRUE,
warnPartialMatchDollar = TRUE,
showWarnCalls = TRUE,
showErrorCalls = TRUE
)
3.6. Quarto
3.6.1. YAML front matter
QuartoやR Markdownでは、YAML front matter(YAMLヘッダー)という形式で、コード処理やレンダリング処理に必要なメタ情報を、knitr
、rmarkdown
およびPandocに渡すことができます。設定の詳細はQuartoの公式ドキュメントを見ていただくのが一番良いと思います。どんな設定でもいいと思いますが、以下の項目は比較的オススメです。
項目 | 説明 |
---|---|
self-contained: true |
htmlファイルに画像データなどを埋め込みます。解析レポートの共有に便利です。 |
fig-format: svg , extract-media: "./figures"
|
プロットの画像データをsvg形式でエクスポートします。 |
bibliography |
引用文献の書誌要素を記述した.bib ファイルを参照します。 |
csl |
引用文献のスタイル定義を記述した.csl ファイルを参照します。 |
---
title: "Analysis report"
subtitle: " `r Sys.time() ` "
author: "Your Name"
format:
html:
toc: true
number-sections: true
theme: simplex
backgroundcolor: "#e3e3e3"
monobackgroundcolor: "#ffffff"
highlight-style: solarized
code-block-bg: "#dcd6d2"
mainfont: Times New Roman, YuMincho, Hiragino Mincho ProN, Yu Mincho,
MS PMincho, serif;
linestretch: "2.5"
self-contained: true
code-tools: true
code-fold: true
link-external-icon: true
link-external-newwindow: true
fig-format: svg
fig-width: 10
extract-media: "./figures"
bibliography: "./templates/0000_r-project.bib"
csl: "./templates/sage-vancouver.csl"
---
3.6.2. セットアップチャンク
checkpoint
パッケージをアタッチしてチェックポイント日を指定した後、必要なパッケージを library()
や p_load()
といった関数で読み込むと、指定した日付のスナップショットが読み込まれます。
```{r}
#| label: setup
#| code-fold: false
# attach the package 'checkpoint'
library(checkpoint)
# set checkpoint date
checkpoint("2022-04-01", checkpoint_location = "../", scan_now = FALSE)
# attach packages
library(pacman)
p_load(kableExtra, tidyverse)
# save default graphics parameters
oldpar <- par(no.readonly = TRUE)
# load data sets
df_01 <- read.csv("datasets/01_bcg.csv", na.strings = c("", "NULL"))
```
4. 解析レポートの出力例
4.1. Title
解析レポートをレンダリングした日時が記載されます。引用文献はZoteroの「クイック・コピー」機能を使用すれば、Zotero上のレコードをRStudio Serverのエディターにコピーアンドペーストするだけで、Markdown書式で引用できます。
4.2. Table
KableExtra
パッケージを使用した出力例です。このまま、Microsoft Wordに書式なしの表として貼り付け、編集できます。
4.3. Figure
ここでは、タブセットを用いて、データセット、解析結果、複数のFigureを切り替えられるようにしています。Figureをコピーするとsvg形式で保存できます。また、Figureは所定のフォルダーにsvg形式で出力されます。
だいたい、こんな感じです。工夫次第で色々できると思います。
-
プロトコルを公開すること自体は解析計画の妥当性を保証しませんが、恣意的な解析手法の変更を防ぐうえで有用です。 ↩︎
-
データセットやソースコードを公開すること自体は解析結果の妥当性を保証しませんが、解析結果を検証・批判可能な状態にし、将来の累積研究のハードルを下げるうえで有用です。 ↩︎
-
少し昔の論文ですが、計算機科学の再現性について述べた2011年の論文の、Fig.1が大変わかりやすいです。少なくとも、論文を出しただけでは不十分ということですね。Science. 2011 Dec 2;334(6060):1226-7. ↩︎
Discussion