システム化するということ
R言語は、データ分析や統計モデリングにおいて非常に強力なツールですが、分析を単に行う場合と、その実装をシステム化する際には、求められるものが大きく異なります。単なる分析では、分析コードが実行できれば十分ですが、システム化する場合には、コードの品質、再現性、保守性の確保が不可欠となります。
R言語での実装をシステム化する際に、エンジニアが特に意識すべきものとして
- パッケージ管理
- lint & format
- test
の3つの側面について解説していきます。
サンプルリポジトリ
本記事で記載している実装を実際にリポジトリに書いているので、こちらを参考にしながら読んでいただくと幸いです。
https://github.com/Yashikab/sample_r_system
ディレクトリ構成
今回のサンプルリポジトリでは下記のディレクトリ構成になっています。
.
├── Dockerfile
├── Makefile
├── README.md
├── hack
│ ├── check_status.R
│ ├── fmt.R
│ ├── install.R
│ ├── lint.R
│ ├── synchronize.R
│ └── test.R
├── renv
│ ├── activate.R
│ ├── library
│ ├── settings.json
│ └── staging
├── renv.lock
└── src
├── fibonacci.R
├── sample.R
└── tests
-
hack
ディレクトリ: 以下で紹介するCIで実行されるスクリプトを格納します -
src
ディレクトリ: コードの実装とテストはすべてここで行います。hack
スクリプトのチェック対象ディレクトリです -
renv
ディレクトリ: パッケージ管理ツールのrenv
を初期化すると作られるディレクトリです -
renv.lock
ファイル:renv
のステータスを管理するlockファイルです
リポジトリをクローンしたときのまずやること
make install
を実行することで renv.lock
のステータスのライブラリバージョンのインストールなどができ、リポジトリのライブラリ状態でローカルの環境構築ができます。
ただ、R自体のバージョンは合わせておく必要があります。(ここも管理できるようにしたいですが僕の力ではわからなかった。。)
パッケージ管理
パッケージ管理の必要性
R言語のエコシステムは非常に活発であり、数多くのパッケージが開発されています。これらのパッケージを活用することで、複雑なデータ分析を効率的に行うことができます。しかし、パッケージのバージョンが異なることで、コードの動作が不安定になったり、再現性が失われたりする問題が発生する可能性があります。
また、チームで共同で開発を行う場合、メンバー間で使用するパッケージのバージョンが異なることで、環境の差異が生じ、エラーの原因となることがあります。
Rでのパッケージ管理
Rのパッケージ管理ツールはいくつかありますが、今回は renv
を利用したいと思います。
renv は、Rのパッケージ管理ツールです。プロジェクトごとにパッケージのバージョンを固定し、再現性のある開発環境を構築することができます。
- snapshot: プロジェクトの現在のライブラリ状態を「lockファイル」に保存します。このファイルは、プロジェクトの依存関係を記録したもので、いわば「レシピ」のようなものです。
- restore: lockファイルの内容に基づいて、プロジェクトのライブラリ環境を再現します。これにより、異なるマシンや、時間の経過によっても、同じ開発環境を構築することができます。
renvの公式ページはこちらを参考にしてください。
また下記のqiita記事が大変わかりやすく参考にさせていただきました。ありがとうございますmm
(参考: https://qiita.com/okiyuki99/items/688a00ca9a58e42e3bfa)
同期チェックスクリプト
システム化する上で、ライブラリが変更された際に同期されている必要がありますが、これを忘れると意味がなくなります。そこで同期がとれているか確認するスクリプトを用意してCI実行時に同期されていない場合は失敗するようにします。
# 同期されているかチェックするスクリプト
library("renv")
# get status
status <- renv::status()
# check if there are any changes
if (status$synchronize) {
message("There are no changes in the project library.")
quit(status = 0)
} else {
# If there are some change, report it and exit with error
message("There are changes in the project library.")
stop("Please run 'renv::snapshot()' to save the changes.")
}
同期用のスクリプト
snapshotを実行して同期をとるスクリプトも用意します。
# snapshotをとるスクリプト
library("renv")
# install on uninstalled packages
renv::install()
# take snapshot
renv::snapshot()
installスクリプト
リポジトリをクローンしたときのまずやることでも書きましたが、ローカルの環境構築をrenvを使って構築するためのスクリプトです。
中身はただ renv::restore()
しているだけです。
library("renv")
renv::restore()
Lint & Format
どの言語にも言えることですが、Lint & Formatは、コードの品質を保ち、開発効率を向上させるために非常に重要な役割を果たします。
Lintの実装
lintr
を利用してLintを記述します。
library("lintr")
result <- lintr::lint_dir("src")
if (length(result) > 0 ) {
print(result)
stop("Linting failed")
} else {
print("Linting passed")
}
Formatの実装
styler
を使用してformatterを実装します。
library("styler")
# format on src directory
styler::style_dir("src")
Test
単体テストを実装することで、コード品質を担保しましょう。
テスト自体は src/tests
フォルダに書かれることを想定して、これらをCIで実行されるようなスクリプトを用意しておきます。
library("testthat")
test_dir("src/tests")
makeファイルの用意
今回はあまり必要性ないですが、他言語の管理もやるときなどはmakefileを使ってまとめたほうがCI実装時に役立ちます。
fmt:
Rscript hack/fmt.R
lint:
Rscript hack/lint.R
test:
Rscript hack/test.R
check_R:
Rscript hack/check_status.R
sync_R:
Rscript hack/synchronize.R
install:
Rscript hack/install.R
githubへのpush前に make sync_R
を実行して常にライブラリの同期をとることを開発ルールに定めましょう。
CI実装
同期チェックをCIに組み込むことで同期を忘れたままシステムがデプロイされることを防ぎます。
今回は、github actionsをCI環境として利用し、組み込み方法を2つ紹介します。
github actionsのrunnerにR環境を構築してCI実行
actionsのライブラリを使用してR環境をrunnerに入れます。
これは github actionsを利用する場合しか参考にならないですが、こっちのほうが実行速度は早いのと手順は簡単です。
以下のようにworkflowを用意します。
name: Renv use directory flow
on:
push:
jobs:
ci-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install R
uses: r-lib/actions/setup-r@v2
with:
r-version: '4.4.1'
- name: Install renv
uses: r-lib/actions/setup-renv@v2
- name: Check status
run: |
make check_R
dockerコンテナにCI環境を入れる
dockerイメージにCI環境をつくって、github actionsでコンテナを実行するパターンです。
github actions以外のCIツールにも適用しやすいですが、dockerビルドに時間がかかり、少し遅いです。
まず Dockerfileをmultistageで用意します。
renvパッケージを引き継ぐには、renvフォルダをコピーし、cacheのパスを環境変数で指定します。
# r-base
FROM r-base:4.4.1 AS base
RUN apt-get update \
&& apt-get install -y --no-install-recommends libxml2-dev \
&& rm -rf /var/lib/apt/lists/*
RUN R -e "install.packages('renv')"
RUN mkdir -p /basedir
WORKDIR /basedir
RUN mkdir -p renv
COPY renv.lock renv.lock
COPY .Rprofile .Rprofile
COPY renv/activate.R renv/activate.R
COPY renv/settings.json renv/settings.json
RUN mkdir renv/.cache
ENV RENV_PATHS_CACHE=renv/.cache
RUN R -e "renv::restore()"
FROM base AS ci
ENV GITHUB_WOKRDIR=/__w/sample_r_system/sample_r_system
RUN mkdir -p /${GITHUB_WOKRDIR}
ENV WORKSPACE=ci
RUN mkdir -p /${WORKSPACE}
WORKDIR /${WORKSPACE}
COPY --from=base /basedir .
COPY hack /${WORKSPACE}/hack
COPY Makefile /${WORKSPACE}/Makefile
ENTRYPOINT [ "make" ]
github actionsでは dockerのtarget ciを dockerhubにビルドしてこれを使って make check_R
を実行します。
name: R sample flow
on:
push:
jobs:
ci-image-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: yashikab/r-sample-ci:latest
target: ci
cache-from: type=registry,ref=yashikab/r-sample-ci:buildcache
cache-to: type=registry,ref=yashikab/r-sample-ci:buildcache,mode=max
ci-check:
needs: ci-image-build
runs-on: ubuntu-latest
container:
image: yashikab/r-sample-ci:latest
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
defaults:
run:
working-directory: /ci
steps:
- name: Checkout
uses: actions/checkout@v4
- name: copy sources
run: cp -r ${GITHUB_WORKSPACE}/src /ci/src
- name: Check R status
run: make check_R
開発風景
パッケージ同期忘れ
githubでPRを作って何かを開発したとします。
ためしに、sample2.R
に stringr
ライブラリを読み込んだ場合を考えます。
このとき分析者のローカルにすでにstringr
が入っていれば、動作確認では動きます。
そのまま同期を忘れてrenvの管理対象ではないまま githubにコードpushしたとします。
その場合は、check_R
によってCIが失敗し、同期されていないことに気づくことができます。
加えたRスクリプト
library("stringr")
x <- "Hello World"
print(stringr::str_length(x))
$ R -e 'install("stringr")'
$ Rscript src/sample2.R
# ----- 出力結果 ----
# - The project is out-of-sync -- use `renv::status()` for details.
# [1] 11
このまま make sync_R
せずにコードpushするとCIが落ちる。
https://github.com/Yashikab/sample_r_system/actions/runs/10362721271/job/28685177409
ローカルで make sync_R
を実行するとrenv.lock
が更新されて再度pushするとCIが通る。
https://github.com/Yashikab/sample_r_system/actions/runs/10362775516
$ make sync_R
# ---- 出力結果 ----
# Rscript hack/synchronize.R
# - The project is out-of-sync -- use `renv::status()` for details.
#
# 次のパッケージを付け加えます: ‘renv’
#
# 以下のオブジェクトは ‘package:stats’ からマスクされています:
#
# embed, update
#
# 以下のオブジェクトは ‘package:utils’ からマスクされています:
#
# history, upgrade
#
# 以下のオブジェクトは ‘package:base’ からマスクされています:
#
# autoload, load, remove, use
#
# - There are no packages to install.
# The following package(s) will be updated in the lockfile:
# # CRAN -----------------------------------------------------------------------
# - stringi [* -> 1.8.4]
# - stringr [* -> 1.5.1]
# - Lockfile written to "~/github/sample_r_system/renv.lock".
参考PR: https://github.com/Yashikab/sample_r_system/pull/1
lintエラー
Rではコードをコメントアウトで残したままにするとコード規約違反になるようです。
ためしに、コードの一部をコメントアウトしてgithubにpushします。
その場合、LintチェックでCIが失敗します。
src/sample.R
でコードをコメントアウトで追加します。
print("Hello World")
# print("Hey!")
この状態でpushするとCIが失敗します。
https://github.com/Yashikab/sample_r_system/actions/runs/10362841396/job/28685491998
参考PR: https://github.com/Yashikab/sample_r_system/pull/2
Test
testに関しても同様にCIで管理されていますが、ちょっと書くのが面倒になったので割愛します笑
参考
- 私のサンプルリポジトリ: https://github.com/Yashikab/sample_r_system
- renv: https://rstudio.github.io/renv/articles/renv.html
- renvのCIへの組み込み方法: https://rstudio.github.io/renv/articles/ci.html
- renvのDockerへの組み込み方法: https://rstudio.github.io/renv/articles/docker.html
- renvに関して参考にさせていただいたqiita記事: https://qiita.com/okiyuki99/items/688a00ca9a58e42e3bfa