7  データ分析パイプライン

データ分析の作業を、ギリシア神話のシーシュポスの岩に例えることがあります。 神々を欺いたシーシュポスは、岩を山の頂上まで転がし、山頂に到達すると岩が転がり落ちてしまうという刑罰を受けていました。

データ分析の作業も同様で、作業が完結し、結果を出したと思っていても、新しいデータが与えられたり、モデルが更新されたら、改めて分析をやり直さなくてはいけません。 たった一つの関数の引数の値が変わっただけでも、再現のためには作業を繰り返すことになります。 この過程で特に厄介なのが、再実行に伴う時間や作業手順の破壊です。

作業手順の破壊とは、結果を導くまでの手順が複雑であるために、再実行時に誤った手順を再現できないことを言います。 データの読み込みから分析結果の出力まで、シンプルな手順であれば再現は容易ですが、データ分析は煩雑になりやすく、中間生成物が多くなる傾向にあるために手順通りに再現をするのが困難です。

作業手順をパイプラインとして構築することで、作業手順の破壊を防ぐことができます。 パイプラインとは、複数の工程を組み合わせた作業手順のことです。 また、パイプラインが整備されることで、作業の効率化が期待できます。 本章では、R言語でパイプライン、特にデータ分析のために利用可能なtargetsパッケージを紹介します。

install.packages(c("targets"))

7.1 targets

さきほどシーシュポスの岩とデータ分析作業の関係を例に挙げましたが、プログラム開発でも状況は同じです。 特にプログラム開発では、コードの変更に伴う再実行が頻繁に発生します。 この問題を解決するためにMakeが利用されてきました。 Makeとは、makefileと呼ばれるファイルにファイル間の依存関係を記述し、ファイルの生成や更新を自動化するためのツールです。

R言語において、makefileのような機能を提供するパッケージとしてtargetsが挙げられます。 targetsはデータ分析のパイプラインを構築し、再現可能な分析を実現するためのパッケージです。 作業手順を明示的に記述することで、作業手順の破壊を防ぐことができます。 またパイプラインの構築に必要な中間生成物の状態を内部的に保存することで、必要な部分だけを実行、作業時間の短縮を実現します。

library(targets)

targetsパッケージによるパイプラインでは、_targets.Rという名前のファイルを用意して、そこにパイプラインの構成、定義を記述します。

以下の例を見てください。 パイプラインは一つのターゲットによって構成されています。 ターゲットとは、パイプラインの中で生成される生成物のことです。 targetsパッケージでは、tar_target()関数を用いてターゲットを定義します。 tar_target()関数の第一引数はターゲットの名前です。第二引数には、ターゲットを生成するためのコードを記述します。

```{r}
# _targets.R
library(targets)
source(here::here("src/functions.R"))
tar_option_set(packages = c("tibble"))
list(
  tar_target(
    mtcars_mod,
    mtcars |> 
      rowid_to_column(var = "car") |> 
      as_tibble()
  )
)
```

_targets.Rをパイプラインとして正しく機能させるには、いくつかの要件があります。

  1. targetsパッケージ自身の読み込み(tar_target()などの関数を利用するため)
  2. パイプラインの実行に必要なパッケージ、関数の読み込み。tar_option_set()関数のpackages引数で指定するか、source()関数で関数を読み込むか、名前空間を宣言した上で利用する必要があります。
  3. パイプライン本体。list()関数で囲まれた内容がパイプラインの本体と見なされます。list()関数の要素として、tar_target()関数でターゲットを定義します。

パイプラインは、このように一つ一つのターゲットを定義していくことで構築されていきます。

7.1.1 パイプラインの実行

_targets.Rが完成したら、tar_make()関数を実行してパイプラインを実行します。 パイプラインは起動中のセッションとは別のプロセスで実行されるため、一からパイプラインの構築が行われることになります。 再現可能なパイプラインであった場合、targetsはエラーを吐かずに終了します。

tar_make()
#> ▶ dispatched target mtcars_mod
#> ● completed target mtcars_mod [0.007 seconds]
#> ▶ ended pipeline [0.048 seconds]

_targets.Rで定義されたターゲットの生成の様子が表示されています。 末尾にended pipelineと表示されれば、パイプラインの実行が正常に終了したことを表します。

パイプラインが構築されると、_targets/フォルダにその情報が記録されます。 ここにはobjectsフォルダがあり、tar_target()関数で定義し、tar_make()関数によって再現されたターゲットが保存されています。

作成されたターゲットを利用するには、tar_load()関数あるいはtar_read()関数を使います。 例えば、この段階ではパイプライン上のmtcars_modは利用できません。 mtcars_mod_targets/objectsに保存されているためです。 tar_load()関数を使って、作業空間にターゲットを読み込んでみましょう。

# ターゲットは読み込まれていない
mtcars_mod
Error in eval(expr, envir, enclos): object 'mtcars_mod' not found
# ターゲットの読み込み
tar_load(mtcars_mod)
head(mtcars_mod)
car mpg cyl disp hp drat wt qsec vs am gear carb
1 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
2 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
3 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
4 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
5 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
6 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1

一時的にオブジェクトを表示する、あるいはターゲットとは別の名前でオブジェクトを保存するときにはtar_read()関数を使います。 tar_read()関数で読み込まれたターゲットは、作業空間には保存されません。

tar_read(mtcars_mod) |> 
  head()
car mpg cyl disp hp drat wt qsec vs am gear carb
1 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
2 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
3 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
4 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
5 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
6 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1

7.1.2 パイプラインの変更

最初のターゲットが正しく生成されたことを確認したので、パイプラインに処理を追加しましょう。 ここでは最初の_targets.Rの状態から、2つのターゲットを追加し、以下のターゲットが含まれます。

  • mtcars_mod: 最初に定義したターゲット。以前の状態と変わりありません。
  • lm_coef_mtcars: データに対してlm()関数を適用し、その係数をcoefficients()関数で取得します。
  • plot_mtcats: ggplot2パッケージを使って、データをプロットします。その上でlm_coef_mtcarsを使って回帰直線を引きます。
```{r}
# _targets.R (2)
library(targets)
tar_option_set(packages = c("tibble", "ggplot2"))
list(
  tar_target(
    mtcars_mod,
    mtcars |> 
      tibble::rowid_to_column(var = "car") |> 
      tibble::as_tibble()
  ),
  tar_target(
    lm_coef_mtcars,
    lm(mpg ~ wt, data = mtcars_mod) |> 
      coefficients()
  ),
  tar_target(
    plot_mtcats,
    ggplot(data = mtcars_mod) +
      aes(wt, mpg) +
      geom_point(color = "red") +
      geom_abline(intercept = lm_coef_mtcars[1], slope = lm_coef_mtcars[2])
  )
)
```

パイプラインが更新されたらtar_make()関数を再度実行します。

# パイプラインの再実行
tar_make()
#> ✔ skipped target mtcars_mod
#> ▶ dispatched target lm_coef_mtcars
#> ● completed target lm_coef_mtcars [0.001 seconds]
#> ▶ dispatched target plot_mtcats
#> ● completed target plot_mtcats [0.009 seconds]
#> ▶ ended pipeline [0.263 seconds]

ここで大事なことは、依存関係を含めて変更のないターゲットは、再実行されないという点と、ターゲットは_targets.Rで定義された順番に実行されるという2点です。 この例では、mtcars_modを生成する処理が変更されていない限り、以前の結果が再利用されることを意味します。 targetsパッケージには、パイプラインの変更に伴う再実行が必要な処理を自動的に判定する機能があります。 作成、再実行が必要なターゲットのみを対象に処理が行われるため、オブジェクトを作り直す手間が省け、効率的なデータ分析が実現します。

実際にtar_make()関数の出力を確認すると、mtcars_modの生成に関わるパイプラインがスキップされていることがわかります。 また、mtcars_modlm_coef_mtcarsplot_mtcatsの順番で実行されることも確認できます。

7.1.3 パイプラインの可視化

targetsパッケージのパイプライン構築は、_targets.Rに記載されたターゲットの変更と依存関係に基づいて行われます。 ここでもう一度、パイプラインに変更を加えてみましょう。 lm_coef_mtcarsの生成を自作関数に置き換えるという内容に置き換えます。

```{r}
# src/functions.R
fit_model <- function(formula, data) {
  lm(formula, data = data) |> 
    coefficients()
}
```
```{r}
# _targets.R (3)
library(targets)
source(here::here("src/functions.R"))
tar_option_set(packages = c("tibble", "ggplot2"))
list(
  tar_target(
    mtcars_mod,
    mtcars |> 
      tibble::rowid_to_column(var = "car") |> 
      tibble::as_tibble()
  ),
  tar_target(
    lm_coef_mtcars,
    fit_model(mpg ~ wt, data = mtcars_mod)
  ),
  tar_target(
    plot_mtcats,
    ggplot(data = mtcars_mod) +
      aes(wt, mpg) +
      geom_point(color = "red") +
      geom_abline(intercept = lm_coef_mtcars[1], slope = lm_coef_mtcars[2])
  )
)
```

このパイプラインで、影響のあるターゲットは何でしょうか。 答えはlm_coef_mtcars自身とそれを利用するplot_mtcatsの2つです。 tar_make()関数を実行すると、これらのターゲットが再実行されることになります。

targetsパッケージには、いくつかのパイプラインの可視化機能があります。 これらの機能を利用して、パイプラインの構造を把握してみましょう。

tar_mermaid()tar_visnetwork()関数は、パイプラインの依存関係をグラフ形式で表示します。 以下はtar_mermaid()関数でパイプラインのグラフをmermaid形式で表示したものです。 Outdatedと表示されているターゲットは、再実行が必要なターゲットを示しています。

tar_mermaid()

graph LR   
    style Legend fill:#FFFFFF00,stroke:#000000;
    style Graph fill:#FFFFFF00,stroke:#000000;
    subgraph Legend
        direction LR
        x2db1ec7a48f65a9b(["Outdated"]):::outdated --- xf1522833a4d242c5(["Up to date"]):::uptodate
        xf1522833a4d242c5(["Up to date"]):::uptodate --- xd03d7c7dd2ddda2b(["Stem"]):::none
        xd03d7c7dd2ddda2b(["Stem"]):::none --- xeb2d7cac8a1ce544>"Function"]:::none
    end

    subgraph Graph
        direction LR
        x4496d6b18b9b395a(["lm_coef_mtcars"]):::outdated --> x3fe6ea6b43f61067(["plot_mtcats"]):::outdated
        x5dc2b28b4b042e08(["mtcars_mod"]):::uptodate --> x3fe6ea6b43f61067(["plot_mtcats"]):::outdated
        x12e88730e39644dc>"fit_model"]:::uptodate --> x4496d6b18b9b395a(["lm_coef_mtcars"]):::outdated
        x5dc2b28b4b042e08(["mtcars_mod"]):::uptodate --> x4496d6b18b9b395a(["lm_coef_mtcars"]):::outdated
    end
    classDef outdated stroke:#000000,color:#000000,fill:#78B7C5;
    classDef uptodate stroke:#000000,color:#ffffff,fill:#354823;
    classDef none stroke:#000000,color:#000000,fill:#94a4ac;
    linkStyle 0 stroke-width:0px;
    linkStyle 1 stroke-width:0px;
    linkStyle 2 stroke-width:0px;

tar_outdated()関数でもパイプラインの変更による影響を受けるターゲットを確認できます。

tar_outdated()
#> [1] "plot_mtcats"    "lm_coef_mtcars"

それではtar_make()を実行します。 グラフやtar_outdated()関数で示されたターゲットが再構築される様子が観察できます。

tar_make()
#> ✔ skipped target mtcars_mod
#> ▶ dispatched target lm_coef_mtcars
#> ● completed target lm_coef_mtcars [0.002 seconds]
#> ▶ dispatched target plot_mtcats
#> ● completed target plot_mtcats [0.091 seconds]
#> ▶ ended pipeline [0.406 seconds]

ここでパイプラインは完成です。 _targets.Rに変更を加えない限り、作成したターゲットはtar_read()tar_load()関数でいつでも利用できます。 グラフの状態も改めて確認しておきましょう。

# 最後のtar_make()。変更がないため、すべてのターゲットがスキップされる
tar_make()
✔ skipped target mtcars_mod
✔ skipped target lm_coef_mtcars
✔ skipped target plot_mtcats
✔ skipped pipeline [0.059 seconds]
tar_visnetwork()

targetsパッケージには、他にも多くの機能があります。充実したドキュメントから、さまざまな機能を学ぶことが可能です。 さらにigjitら (2022)Bruno (2023)にも、targetsパッケージの使い方について解説されています。

7.2 targetopia

targetsパッケージには、targetsのパイプラインの枠組みを拡張するパッケージがいくつかあります。 targetsパッケージのフレームワークを利用することで、さまざまな用途・分野に合わせて調整された機能を提供しています。 これらのパッケージ群はtargetopiaと呼ばれます。

以下は、targetopiaに含まれるパッケージの例です。

  • tarchetypes… ターゲットのグループ・パラメータ化やR Markdown、Quartoを使ったドキュメント生成のための機能を提供する
  • stantargets… Stanモデルを含む、ベイズデータ分析のためのターゲットを作成する機能を提供する
  • geotargets… 地理空間データの処理に特化し機能を提供する