株式会社ホクソエムのブログ

R, Python, データ分析, 機械学習

データ分析のワークフローをdrakeで管理して効率的に作業を進めよう

要約

  • drakeパッケージは、GNU makeのようにあらかじめ定義されたワークフローを自動的に実施する仕組みを、Rユーザに馴染みやすいデータフレーム形式で提供する
  • ワークフローの構築と管理、実行はRの関数として提供され、依存関係を可視化する関数も用意される
    • drakeパッケージを使うことで、データ分析でありがちな「再実行」の負担(再計算、コードの保守)を軽減することが可能となる
    • 各オブジェクトは自動的にキャッシュされ、コードや依存関係に変更のない場合はキャッシュが利用される
    • ワークフローの各処理の状況、依存関係を可視化する関数も用意され、ワークフロー管理が容易になる

f:id:u_ribo:20180905064443j:plain

はじめに

データ分析の作業は、試行錯誤を繰り返して進めて行くのが一般的です。

Garrett GrolemundとHadley Wickhamの著書 “R for Data Science” (「Rではじめるデータサイエンス」)では、Rを使ったデータ分析作業の工程を以下の図のように整理しています (FontAwesomeのフォントを使って編集しています)。

f:id:u_ribo:20180831061703p:plain

この図の中で、データ整形と可視化、そしてモデリングの工程がひとまとまりになっており、これらの工程を経て分析結果を伝えるための作業に入ります。

データ整形と可視化、モデリングは互いに影響しあう関係にあり、繰り返し実行されることを想定しています。繰り返しの例としてはモデルに用いる変数のアップデートがあります。その際、モデルを実行するコードを修正するだけでなく、それに関連する前後のプロセス、すなわちモデルデータの生成とモデルの結果を利用するプログラムについても見直す必要があるでしょう。

こうした「繰り返し」や「やり直し」の作業は、みなさんご存知の通り、時間がかかる骨の折れる仕事で、退屈なものです。場合によっては、ほとんど1からコードを書き直すなんて事もあるかもしれません。間に実行時間の長い処理が途中に入る時も、処理が終わるまで待つ必要があります。また、変更箇所に見落としがあるとフローは流れなくなってしまいます。変更が多ければ多いほど、人為的な誤りも犯しやすくなる危険があります。

シーシュポスの岩

f:id:u_ribo:20180831234831g:plain

ギリシア神話の登場人物の一人、シーシュポスは徒労を意味する「シーシュポスの岩」で知られます。シーシュポスの岩とは、神を欺いた罰として巨大な岩を山頂まで運ぶことになったシーシュポスの逸話です。彼が岩を持ち上げ、山頂を目指すと、岩は転がり落ちてしまいます。彼は再び岩を持ち上げ山頂を目指さすのですが、また岩は転落してしまう。これが永遠に繰り返される、というものです。

永遠に続くというのは大げさですが、データ分析の作業もこの話と似ています。

既存の解決策

データ分析の話に関わらず、こうしたパターンのある一連の作業(ワークフロー)を自動化するツールやプログラムがあります。例を挙げると、GNU MakeApache Airflowが有名です。これらのツールでは、ワークフローをあらかじめ記述したファイルを用意しておき、プログラムに自動的に実行させることで、人間の作業負担を軽減することが期待できます。

一方でMakeは全てのRユーザが親しみやすいものではありません(私もそうです)。AirflowもまたPythonにより書かれたファイルのワークフローを想定しています。

Rのパッケージでも、knitrR Markdownのエコシステムの採用、memoiseによるメモ化を使った複数の作業の自動処理が考えられますが、ワークフロー全体の管理としては不十分です。makefileの枠組みをRに導入したremakeも開発されていますが痒いところには手が届きません。

drake: Rユーザのためのワークフロー処理パッケージ

今日の主役となるdrakeもまた、Rでのワークフローを支援することを目的に開発されたパッケージです。

その特徴は、ワークフローをデータフレームとして管理する点にあります。データフレームはRユーザが日頃から扱っているオブジェクトクラスであるため、導入の敷居が低いのが利点です。drakeではこのデータフレームをワークフローデータフレームと呼びます。

drakeによるワークフローの管理と実行はそれぞれ、drake_plan()make()で行います。そのため、Rによる作業のワークフローをRだけ、つまり.Rファイルの中で完結することが可能です。drakeには今挙げた関数の他に、ワークフロー内の各タスクの依存関係を描画する関数など、たくさんの関数がありますが、ひとまずはこの2つの関数の使い所を捉えましょう。

f:id:u_ribo:20180901142448p:plain

前置きが長くなりましたが、以降でdrakeパッケージによるワークフローの実践例をみていきましょう。

ワークフロー管理の基礎

前回の記事で扱った、 「架空物件の費用と物件の広さの関係」を線形回帰モデルに当てはめる処理を例に取り上げます。

blog.hoxo-m.com

この記事で書いた対数線形回帰モデルの実行処理を一つのワークフローとして考えますが、最初は簡単に、1. 分析に必要なサンプルデータを読み込む、2. 回帰分析を行う、の2点に限定して考えてみます。この工程をコードで示すと以下のようになります。

library(readr)

df_rental <-
  read_csv("https://raw.githubusercontent.com/MatsuuraKentaro/RStanBook/master/chap07/input/data-rental.txt")

lm(formula = log(Y) ~ log(Area), data = df_rental)

f:id:u_ribo:20180905183103p:plain

このフローをdrakeで実行してみましょう。まずはdrake_plan()でワークフローデータフレームを構築します。といっても、基本的には先に示したコードをdrake_plan()内に格納するように記述するだけです。処理の間はカンマで区切ります。コードを簡略化した下記の図の方がわかりやすいと思います。

f:id:u_ribo:20180901155555p:plain

既存のコードをdrake_plan()に引き渡すのは簡単なのですが、注意点が2箇所あります。最初の例ではオブジェクトへの代入に <- 演算子を用いましたが、drake_plan()の中では = に置き換えます。これはtarget = command の関係を明らかにするためです。加えて、引数strings_in_dotsに"literals"を指定してください。文字列を囲む引用符が二重引用符として与えられているのを一重引用符として変換するのを防ぐのに指定します(指定しなくても問題ない時もありますが、警告が出ます)。

library(drake)

df_plan <- 
  drake_plan(
    df_rental =
      read_csv("https://raw.githubusercontent.com/MatsuuraKentaro/RStanBook/master/chap07/input/data-rental.txt"),
    lm(formula = log(Y) ~ log(Area), data = df_rental),
    strings_in_dots = "literals")

作成したワークフローデータフレームを確認します。

df_plan
## # A tibble: 2 x 2
##   target        command                                                   
## * <chr>         <chr>                                                     
## 1 df_rental     "read_csv(\"https://raw.githubusercontent.com/MatsuuraKen…
## 2 drake_target… lm(formula = log(Y) ~ log(Area), data = df_rental)

ワークフローデータフレームは、ワークフローを実行するためのtargetとcommandからなることは先に述べた通りです。 target = commandの形式にしなかった部分には、targetの列に自動的に名前がつけられています。

一方でこの段階ではまだcommandの評価されていません。対象のワークフローデータフレームをmake()に与えて実行することで評価が行われます。

make(df_plan)
## target df_rental

## Parsed with column specification:
## cols(
##   Y = col_double(),
##   Area = col_double()
## )

## Target df_rental messages:
##   Parsed with column specification:
## cols(
##   Y = col_double(),
##   Area = col_double()
## )

## target drake_target_1

make()を実行すると、ワークフローに記述した処理が走ります。ただしメッセージの出力はあるものの、lm()の結果は表示されていません。これはどういうことでしょうか。

drakeでは、make()の結果とワークフローの内容をキャッシュしています(.drakeというフォルダに格納されます)。ワークフローに記述したターゲットおよびターゲットが依存するターゲットに変更がない限り、一度実行した結果を再現可能なのがdrakeの特徴です。これは改めてmake(df_plan)を実行するとよくわかります。

make(df_plan)
## All targets are already up to date.

今度は"All targets are already up to date."と出力されました。このメッセージはワークフローに変化がなかったことを意味します。

ではmake()の結果を呼び出してみましょう。これにはreadd()またはloadd()を使います。いずれも、キャッシュされた結果を再現する関数ですが、readd()が値を返すだけなのに対して、loadd()は値を環境中にオブジェクトとして保存します。関数の第一引数には、値を取得するtarget名を指定します。

f:id:u_ribo:20180905001354p:plain

次の処理は、lm()の結果を出力し、物件データをオブジェクトとして保存します。

ls()
## [1] "df_plan"
df_plan$target
## [1] "df_rental"      "drake_target_1"
readd(drake_target_1) # 結果の出力
## 
## Call:
## lm(formula = log(Y) ~ log(Area), data = df_rental)
## 
## Coefficients:
## (Intercept)    log(Area)  
##       1.841        1.107
loadd(df_rental)  # オブジェクトとして保存
ls()
## [1] "df_plan"   "df_rental"

では、 drake_plan()でワークフローデータフレームを修正してから改めてmake()を行ってみましょう。下記のコードでlm()の処理にlm_resという名前をを与え、対数変換の処理を除外するようにプランを変更します。

df_plan <- 
  drake_plan(
    df_rental =
      read_csv("https://raw.githubusercontent.com/MatsuuraKentaro/RStanBook/master/chap07/input/data-rental.txt"),
    lm_res = lm(formula = Y ~ Area, data = df_rental),
    strings_in_dots = "literals")

make(df_plan)
## Unloading targets from environment:
##   df_rental

## All targets are already up to date.

今度の出力では、ワークフローデータフレームに変更を加えた部分の、lm_resが表示されました。この出力は、lm_resという名前のターゲットが更新されたことを示しています。確認のため、readd()で値を復元しましょう。

readd(lm_res)
## 
## Call:
## lm(formula = Y ~ Area, data = df_rental)
## 
## Coefficients:
## (Intercept)         Area  
##     -147.89        13.99

drake_plan()の内容を書き換えた通り、対数変換を行わない値を用いた結果が得られました。一方でターゲットに変更のないデータ読み込みの部分、df_rentalについては評価が行われていません。変更が加えられていないターゲットについては、一度make()で作られたキャッシュの値が再利用されます。これは冒頭に述べたような、やり直しのある作業を実行するのに適していて、Rによるデータ分析時のオブジェクトの生成に時間のかかる処理やAPIリクエストのように制限がある処理を扱うのに大変便利な仕組みです。

f:id:u_ribo:20180905064443j:plain

ワークフローと依存関係の可視化

データ分析の手順は複雑になりがちです。どのオブジェクトがどの処理に使われているか、久しぶりに開いたファイルでは覚えていないこともあります。drakeでは、ワークフローの各targetとそれに使われるファイルやオブジェクト、関数の依存関係を可視化する機能を用意しています。この機能を使うことで、簡単にワークフローの流れを把握し、不備がある場合はそれを発見しやすくなります。

可視化の方法にはいくつかの関数が用意されていますが、まずはdrake_config()に実行予定のワークフローデータフレームを与えるのが前段階となります。

config <- 
  drake_config(df_plan)

ここではvis_drake_graph()の例を示します。この関数は、ワークフローを有向非巡回グラフ (Directed Acyclic Graph; DAG)として描画します。可視化の関数は他に、sankey_drake_graph()、ggplot2をベースにしたdrake_ggraph()がありますが、ここでの説明は割愛します。

vis_drake_graph(config)

f:id:u_ribo:20180905070803g:plain

このワークフローは単純なものですが、三角で示された2つの関数read_csv()lm()により、最終結果であるlm_resを生成している過程がわかるかと思います。lm_resが依存しているのはデータであるdf_rentalとlm()です。またdf_rentalはread_csv()に依存することを示しています。

ここまでがdrakeの基本的な関数とその説明です。最後に、より踏み込んだdrakeの仕組みを理解するために、架空物件のモデルを複雑にしてみましょう。

ワークフローの変更

元の記事ではrecipesパッケージを使って対数変換したデータを生成し、生成されたデータを直接lm()に与えました。この処理をワークフローに追加してみます。

実際のデータ分析でも、こういったワークフローの変更が頻繁に行われる事かと思います。普通、データの再作成には手間がかかりますが、drakeを使う事で変更のないtargetに関しては自動的に値を使うことができ、時間の短縮に繋がります。

ワークフローデータフレームはbind_plans()を使う事で複数のワークフローを一つにすることができます。またその際、オブジェクト生成の順番はmake()時に必要なオブジェクトを探すという手続きが取られるため、特に順番を気にせずにフローの作成に集中できます。

追加するワークフローをdrake_plan()に記述します。recipesの関数を使った処理の詳細は前回の記事をご覧ください。線形モデルに使う目的変数、説明変数を変数変換した値を生成する処理と生成されたデータを線形モデルに当てはめる、2つのワークフローデータフレームを作っています。

f:id:u_ribo:20180905183545p:plain

library(recipes)
df_model_plan <- 
  drake_plan(
  mod_rec = df_rental %>% 
      recipe(formula = Y ~ Area) %>% 
      step_log(all_predictors(), all_outcomes()),
    rec_trained = 
      prep(mod_rec, retain = TRUE, verbose = TRUE),
    df_rental_log =
      rec_trained %>% 
      juice())

df_out_plan <- 
  drake_plan(
    log_lm_res = 
      lm(formula = Y ~ Area, data = df_rental_log) %>% 
      tidy()
  )

複数のワークフローデータフレームはbind_plans()で一つにまとめることが可能です。新たにワークフローデータフレームを作成したら、今回はmake()の実行前にtarget間の依存関係を見てみましょう。

df_rental_lm_plan <- 
  bind_plans(df_plan, df_model_plan, df_out_plan)
config <- 
  drake_config(df_rental_lm_plan)

vis_drake_graph(config)

f:id:u_ribo:20180905071054p:plain

今度の図は、緑の丸の他に黒丸があるのが目立ちます。黒丸の凡例は"outdated"となっています。この場合、ワークフローを作成してからmake()を実行していないため、追加したtargetに関してはこのようになっています。

それではmake()を実行してみましょう。この例では効果がないかもしれませんが、drakeではワークフローを並列処理するオプションも用意されています。これにはjobs引数を有効にします。

make(df_rental_lm_plan)
## target mod_rec

## target rec_trained

## oper 1 step log [training]

## target df_rental_log

## target log_lm_res
# 並列処理
# make(df_rental_lm_plan, jobs = 2)

df_planについては実行済みですが、新たにreciepsの処理が追加されたことにより、その部分が評価され、結果がキャッシュされます。

recipesで作成したデータをlm()に適用した結果を確認します。普通の作業では一つ一つの関数を実行していきますが、drakeを使った場合はdrake_plan()およびmake()のみを実行するだけで結果を得ることができます。途中に作成するオブジェクトも、必要に応じて再現することが可能です。

readd(lm_res) %>% tidy()
    ## # A tibble: 2 x 5
    ##   term        estimate std.error statistic  p.value
    ##   <chr>          <dbl>     <dbl>     <dbl>    <dbl>
    ## 1 (Intercept)   -148.     28.5       -5.18 1.17e- 6
    ## 2 Area            14.0     0.693     20.2  1.10e-36
readd(log_lm_res)
    ## # A tibble: 2 x 5
    ##   term        estimate std.error statistic  p.value
    ##   <chr>          <dbl>     <dbl>     <dbl>    <dbl>
    ## 1 (Intercept)     1.84    0.196       9.38 2.73e-15
    ## 2 Area            1.11    0.0557     19.9  3.72e-36

長くなりましたが、このようにdrakeを使うことでRの作業を自動的に管理・実行しやすくなります。複雑で時間のかかる分析プロジェクトにぜひ導入してみてはいかがでしょうか。ここでは紹介しきれていない、トリガーや高速処理のための機能もあります。

またdrakeはサポート体制がしっかりしており、exampleも豊富です(drake_example())。またGitHub Issuesやマニュアルも用意されています。下記に参考のURLを紹介しておきます。

Enjoy!

参考URL