6  再現性を高めるRパッケージ

Rでの分析環境の再現性を保障するには、いくつかの要素が重要です。 例えば利用するパッケージのバージョンや、データの変更、環境の違いなどが再現性を阻害する要因となります。 幸いなことに、これらの問題に対処するためのRパッケージが提供されています。 hereやreprexなど、いくつかのRパッケージについてはすでに取り上げていますが、 この章では、再現性の問題となりやすい要因について、さらに詳しく解説し、それらに対処するためのRパッケージを紹介します。

本章で扱うパッケージは以下のコードでインストールされます。

install.packages(c("renv", "pointblank", "pins", "config"))

6.1 renvパッケージ

複数環境でRを実行する際は、ある環境では必要なパッケージがインストールされていなかったり、インストール済みであってもバージョンが異なることがあります。 このような環境間でのパッケージの違いは、対象のパッケージの特定のバージョンをインストールすれば解決することですが、パッケージが多い場合はその作業が苦痛になります。

renvパッケージは、プロジェクトごとに必要なパッケージを管理するためのRパッケージです。 通常は一つのパッケージに対して利用可能なバージョンは一つだけですが、renvを用いれば、プロジェクトごとに異なるバージョンのパッケージを利用可能になります。 その仕組みは、renvを使ってインストールされるパッケージは、通常のRのパッケージのインストール先とは異なる場所にインストールされるためです。 renvを使わないプロジェクトに影響することはありません。

インストールされるパッケージは将来利用するプロジェクトでも再利用されます。 たくさんのパッケージを利用するプロジェクトでは、本来すべてのパッケージを揃えるのに時間がかかりますが、renvではキャッシュ機能によって、再インストール時の時間の短縮化を行います。

6.1.1 renvプロジェクトの開始

renvでパッケージの管理を行うには、以下のコードを実行し、準備する必要があります。

# renvパッケージの読み込み
# renvパッケージの関数名は短いものが多いため、ここではパッケージ読み込み後も renv::関数名の表記を行う
library(renv)
renv::init()

renv::init()関数の実行により、プロジェクト中に.Rprofilerenv.lockの2つのファイルとrenvフォルダが作成されます。 .Rprofileにはrenvによる管理を行うことを示す1行分のコードが記述されいます。 renv::init()を実行する前に、すでに.Rprofileが存在する場合、1行追加されるだけで既存の内容は残ります。

バージョン管理システムを使っている場合、renv.init()関数で生成されたファイルはいずれもバージョン管理に含めることになります。 .Rprofileに記録されたrenv利用のコードがなければ、自分以外はrenvを利用できませんし、renv.lockがなければ、他の人が同じ環境を再現することができないためです。

6.1.2 パッケージの管理

renvを使ったプロジェクトでは、保存されたRファイルやRmd(.rmd)、.qmdの中で使われるRパッケージを管理対象にします。renv::status()`関数を使うと、プロジェクト中のソースコードを再現するのに必要なパッケージのインストール状況について、下記のような確認が行われます。

renv::status()
#> The following package(s) are in an inconsistent state:
#> 
#>  package     installed recorded used
#>  clipr       y         n        y   
#>  credentials y         n        y   
#>  gert        y         n        y   
#>  gh          y         n        y   
#>  gitcreds    y         n        y   
#>  ini         y         n        y   
#>  reprex      y         n        y   
#>  usethis     y         n        y   
#>  zip         y         n        y   
#> 
#> See ?renv::status() for advice on resolving these issues.

installedはインストール状態を示す列です。 yならパッケージはインストールされています。 recordedはrenv.lockに記録されているかどうかを示します。 上記の例ではいずれもnとなっています。 usedはプロジェクトで利用されているかどうかを示します。

次に行う行動は、installednの未パッケージをインストールし、 recordedがn、usedがyのパッケージをrenv.lockに記録することです。 これらの作業はrenv::snapshot()関数を使って行います。

renv::snapshot()
#>  The following package(s) will be updated in the lockfile:
#>  
#>  # CRAN -----------------------------------------------------------------
#>  - clipr         [* -> 0.8.0]
#>  - credentials   [* -> 2.0.1]
#>  - gert          [* -> 2.1.1]
#>  - gh            [* -> 1.4.1]
#>  - gitcreds      [* -> 0.1.2]
#>  - ini           [* -> 0.3.1]
#>  - reprex        [* -> 2.1.1]
#>  - usethis       [* -> 3.0.0]
#>  - zip           [* -> 2.3.1]
#>  
#>  Do you want to proceed? [Y/n]: 

renv::snapshot()の出力は、renv.lockの記録とCRANやリモートリポジトリのバージョンに基づいて行われます。 [* -> 0.8.0]のように、矢印で示されている先が、新しく記録されるバージョンを示します。 対して矢印の元になる方はrenv.lockに記録されたバージョンを示しています。 *renv.lockに記録がないことを表します。 すでにrenv.lockに記録があるパッケージはそのバージョンが表示されます。

記録を行うことに問題はないので、Yを入力してEnterキーを押します。 これによりrenv.lockの更新が確定されます。

プロジェクトでの作業を進めていくと、利用するパッケージが次第に変化します。 そのためrenv.lockを定期的に更新することが重要です。 手順は今見てきたように、renv::status()での状況確認、続いてrenv::snapshot()関数でのバージョンを参考にしながらのrenv.lockの更新です。

renv::snapshot()関数実行時に、もしスナップショットからの変更がないのであれば、出力は以下のようになります。

renv::snapshot()
#> - The lockfile is already up to date.

6.1.3 パッケージの追加・削除

renvでは、renv::snapshot()関数を使って自動的に利用されるパッケージをインストールしますが、ここでは原則として最新版がインストール対象となります。 しかし、特定のバージョンやGitHubの開発版を利用したい場面もあります。 このような状況のために、ユーザーが手動でパッケージの追加・削除を行うことも可能です。

パッケージの追加は install.packages()関数か renv::install()関数を使って行います。 前者は通常のRと同じインストール方法です。 どちらの関数を使っても挙動は同じです。 ただし、renv::install()関数では、CRANにあるパッケージだけでなく、そのバージョンを指定することもできます。 また、GitHubやBioconductor上のパッケージもインストールの対象とすることが可能です。

# CRAN上のパッケージのインストール
# @以降にバージョンを指定することで、特定のバージョンをインストールできる
renv::install("jmastats@0.2.2")

# GitHub上のパッケージをインストール
# GitHubのユーザー名/リポジトリ名の形式を指定する
renv::install("uribo/jpmesh")

パッケージの追加を行ったらrenv::snapshot()関数を実行して、プロジェクトのスナップショットを取りましょう。 適切にパッケージがインストールされている場合、renv.lockが更新されます。

パッケージを削除するには、renv::remove()関数にパッケージ名を指定して実行します。 ただし、プロジェクト中のソースコードのどこかで削除したパッケージ、関数の呼び出しがある場合、renv::snapshot()関数を行ったときに再びインストールを促す出力が行われます。 完全にパッケージをプロジェクトで利用しないというときは、ソースコードからも削除することをお勧めします。

# jpmeshパッケージを削除する
renv::remove("jpmesh")
renv::snapshot()
#> The following required packages are not installed:
#> - jpmesh
#> Packages must first be installed before renv can snapshot them.
#> Use `renv::dependencies()` to see where this package is used in your project.
#> 
#> What do you want to do? 
#> 
#> 1: Snapshot, just using the currently installed packages.
#> 2: Install the packages, then snapshot.
#> 3: Cancel, and resolve the situation on your own.

ここで1を選択すると、renv.lockからの削除も行われます。 一方、2を選ぶと再度インストールを実行し、renv.lockの更新が行われます。

6.1.4 パッケージの復元

リモートリポジトリからのクローンや、他の環境からrenvを使ったプロジェクトで作業を始める際は、renv::restore()関数によってrenv.lockに記録されたパッケージをインストールします。 これにより、プロジェクトで使われるパッケージが揃い、再現性の向上に役立ちます。 もし他のプロジェクトで同一パッケージの同一バージョンが利用されていた場合、キャッシュが使われるためにインストール時間が短縮されます。

6.1.5 パッケージの更新

データ分析を進めていくと、パッケージの新しいバージョンがリリースされることがあります。 これらのパッケージの更新は、renv::update()関数を使って行います。 個別のパッケージを指定して、更新を確認することもできますが、引数に何も指定しない場合は、renv.lockに記録された全てのパッケージに対して更新の有無を確認します。

renv.lockに記録されたバージョンと異なるバージョンがCRANやGitHubに登録されている場合、renvは更新を促すメッセージを表示します。 renv::snapshot()関数のときと同様、[0.1.0 -> 0.1.1]のように具体的なバージョンの変更が表示されます。 更新を行うかどうかはユーザーの判断で行います。 また、更新したバージョンのrenv.lockへの反映は、renv::snapshot()関数を実行することで行われます。

# パッケージの更新を確認
renv::update()
#> - Checking for updated packages ... Done!
#> - All packages appear to be up-to-date.

この例では、すべてのパッケージが最新であり、更新が必要なものはないと表示されています。

6.2 pointblankパッケージ

データ分析を行う上で、データは変化することがあります。 特定のデータについて問題が見つかったために除外されたり、新たなデータが得られたりすることがあるからです。 データが更新されること自体は珍しいことではありませんが、変更内容によっては分析結果に大きく影響を及ぼすことがあり、注意が必要です。

また、データ分析の過程で、データの品質に問題が生じることもあります。 これには、予想外の値や欠損値の混入、データの形式の不整合などが含まれます。 これらの問題に気がつかないまま分析を進めると、誤った結果を導くことになりかねません。 そのため、データの品質は常に確認しておくべきです。

次に紹介するパッケージは、データの品質を検証するpointblankパッケージです。 データフレームに対して、事前に用意したさまざまな検証関数を適用することで、データの変化による問題を検知することを可能にします。

# pointblankパッケージの読み込み
library(pointblank)

pointblankパッケージによる検証は、その場での実行と、レポート形式での出力の2つの方法で行われます。 検証対象は、ローカル環境にあるデータフレームが前提となりますが、データベースやSparkなどのリモートデータに対しても検証が可能です。

多様な検証関数や、出力形式、リモートデータの指定などの詳細は、公式ドキュメントを参照してください。 ここではまず、pointblankによるデータ検証の基本的な手順を紹介します。

例として、mtcarsデータを対象にデータ検証を行いますが、準備として以下の処理で、行名をcar列として取り扱うようにし、tibble形式に変換しておきます。

mtcars_mod <- 
  mtcars |> 
  tibble::rownames_to_column(var = "car") |> 
  tibble::as_tibble()

6.2.1 データ検証の基本

pointblankのデータ検証は、データフレームに検証関数を適用する形で実行されます。 検証関数は、データの品質・状態を確認するための関数で、データの行数や列数、特定の列の値の範囲などを検証します。 以下のコードでは、row_count_match()関数を使って、データフレームの行数が指定した値と一致するかどうかを確認しています。 予期された件数と一致する場合、検証は成功となり、データフレームの内容が返却されます。

mtcars_mod |> 
  row_count_match(32L)

では、検証に失敗するとどうなるか見てみましょう。 次のコードでは、col_vals_between()関数を使って、disp列の値が75から470の範囲に収まるかを検証しています。

mtcars_mod |> 
  col_vals_between(vars(disp), 75, 470)
Error: Exceedance of failed test units where values in `disp` should have been between `75` and `470`.
The `col_vals_between()` validation failed beyond the absolute threshold level (1).
* failure level (2) >= failure threshold (1)

エラーメッセージが出力され、処理が停止しました。 pointblankによる検証は、事前に用意したデータの品質と相違がある場合、エラーを返すことで問題を通知します。 これにより、意図しないデータの変更を防ぎ、分析結果の信頼性を高めることができます。

実際にmtcarsデータのdispの範囲がどうなっているか、確認し、先ほどのコードを修正してみましょう。

range(mtcars_mod$disp)
[1]  71.1 472.0
# range()関数の出力に基づき、範囲を修正する
mtcars_mod |> 
  col_vals_between(vars(disp), 71, 472)

今度は問題なく検証が完了しました。

検証関数は、複数の関数を組合せて使うこともできます。 パイプ演算子を用いてコードを記述すると、検証の手順が明確になり、データの品質を確認しやすくなります。 行数と範囲の検証を一つの処理で実行します。

mtcars_mod |> 
  row_count_match(32L) |> 
  col_vals_between(vars(disp), 71, 472)

検証で不備が見つかるとエラーになることは既に確認しましたが、 複数の検証項目について検証を行う場合には、処理を継続させる必要があります。 そのためのオプションとして、actions引数での検証診断結果の閾値を調整できます。 actions引数には、action_levels()関数あるいはwarn_on_fail()関数などのヘルパー関数を与えて、検証結果に対するアクションを指定します。

# warn_on_fail()関数でも同様
al <- 
  action_levels(warn_at = 1)

# 検証は失敗するが、エラーではなく警告として処理される。
mtcars_mod |> 
  col_vals_between(vars(disp), 75, 470, actions = al)
Warning: Exceedance of failed test units where values in `disp` should have been between `75` and `470`.
The `col_vals_between()` validation failed beyond the absolute threshold level (1).
* failure level (2) >= failure threshold (1)

6.2.2 エージェントによる検証とレポートの作成

次に、エージェントと呼ばれる仕組みを用いて、データ検証を行う方法を紹介します。 エージェントを利用することで、検証結果をレポートとして出力することができます。

はじめにcreat_agent()関数でエージェントを作成し、その後検証関数を適宜追加していきます。

al <- action_levels(warn_at = 0.1, stop_at = 0.2)

agent <- 
  create_agent(mtcars_mod,
               label = "pointblankによるデータ品質検証",
               actions = al) |> 
  col_vals_not_null(vars(mpg)) |>
  col_vals_in_set(car, set = c("Merc 450SL")) |> 
  col_is_numeric(vars(cyl, vs, am, gear)) |> 
  col_is_factor(vars(carb)) |> 
  col_exists(price)

検証項目の追加が完了したら、interrogate()関数を使って検証を実行します。 検証が完了すると、検証結果がレポート1として出力されます。

interrogate(agent)
Pointblank Validation
pointblankによるデータ品質検証
tibble mtcars_modWARN 0.10 STOP 0.20 NOTIFY
STEP COLUMNS VALUES TBL EVAL UNITS PASS FAIL W S N EXT

1
col_vals_not_null
 col_vals_not_null()

mpg

32 32
1
0
0

2
col_vals_in_set
 col_vals_in_set()

car

Merc 450SL

32 1
0.03125
31
0.96875

3
col_is_numeric
 col_is_numeric()

cyl

1 1
1
0
0

4
col_is_numeric
 col_is_numeric()

vs

1 1
1
0
0

5
col_is_numeric
 col_is_numeric()

am

1 1
1
0
0

6
col_is_numeric
 col_is_numeric()

gear

1 1
1
0
0

7
col_is_factor
 col_is_factor()

carb

1 0
0
1
1

8
col_exists
 col_exists()

price

1 0
0
1
1

2024-09-02 00:40:20 UTC < 1 s 2024-09-02 00:40:20 UTC

6.2.3 データのスキャン

検証関数を用意するには、事前にデータの状態を把握しておかなくてはなりません。 列の多いデータフレームでこれを行おうとすると手間がかかります。 そこでscan_data()関数の利用が有効です。 scan_data()関数は、データを分析し、検証に役立つ以下の項目を抽出し、レポートにまとめます。

  • 概要 (Overview): データのサイズ、欠損値を含む行数、重複のある行数、列の種類
  • 変数 (Variables): 変数の要約統計量
  • 相互作用 (Interactions): 変数の確率密度、変数間の散布図を含んだ散布図行列
  • 相関 (Correlations): 数値変数の相関行列の図
  • 欠損値 (Missing): 変数の欠損具合を示す図
  • サンプル (Sample): データの先頭行と末尾行

レポートに含める項目は、必要に応じて取捨選択や並び替えが可能です。 scan_data()関数のsections引数を使用すると、特定の項目だけを指定できるようになります。 指定される内容は、対象とする各項目の大文字頭文字を組み合わせた文字列です。 既定値として、すべての項目を含める”OVICMS”が与えられていますが、例えば概要と変数のみをレポートにまとめるのであれば”OV”を指定します。

# データの概要と変数についてレポートを行う
scan_data(mtcars_mod, sections = "OV")

6.2.4 その他のデータ検証パッケージ

pointblankは、データ検証のためのパッケージの一つですが、他にも多くのパッケージが存在します。 validateパッケージは、pointblankと同様にデータフレームに対する検証を可能としますが、シンプルな関数の中での細かな検査項目の指定を行う点が特徴です。 検査結果を示すグラフの描画機能も備えています。 このパッケージの使い方について、Mark van der Loo and Edwin de Jonge(地道ら訳)(2022)に詳しく書かれています。

6.3 pinsパッケージ

6.4 configパッケージ


  1. ドキュメントの章で取り上げたgtテーブルが利用されています。↩︎