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

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

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

要約

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

f:id:u_ribo:20180905064443j:plain

  • 要約
  • はじめに
    • シーシュポスの岩
    • 既存の解決策
  • drake: Rユーザのためのワークフロー処理パッケージ
    • ワークフロー管理の基礎
    • ワークフローと依存関係の可視化
    • ワークフローの変更
  • 参考URL

はじめに

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

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

f:id:u_ribo:20180831061703p:plain

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

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

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

続きを読む

【2018年版】R でハッシュテーブルの速度比較

以前、こういう記事を書いた。

Rでハッシュテーブルを使う方法はいくつかあるが、

  • サイズが 1000 以下ならば名前付きベクトルが速い
  • それ以上なら環境を使った方法が速い
  • hash パッケージは遅いが記述がわかりやすい

ということがわかった。

今回はさらに高速なハッシュテーブルを実装するパッケージを3つ見つけたのでこれらを加えて比較してみる。

使用するデータは前回と同じく次のコードで生成した。

generate_random_labels <- function(num, length = 10) {
  apply(Vectorize(sample, "size")(letters, rep(num, length), replace = TRUE), 1, function(row) paste(row, collapse = ""))
}
generate_random_values <- function(labels, digits = 1) {
  format <- paste0("%s%0", digits, "d")
  sprintf(format, labels, Vectorize(sample, "size")(seq_len(10^digits-1), rep(1,length(labels))))
}

N <- 1000
labels <- generate_random_labels(N)
values <- generate_random_values(labels)
df <- data.frame(labels, values, stringsAsFactors = FALSE)

target <- sample(labels, 5)
target
#> [1] "eguiqhmukh" "yuvlnhfvba" "cjbcajffur" "nttsfcuszh" "ibvgfrjnro"

1. 名前付きベクトル

名前付きベクトルは最もシンプルなハッシュテーブルの実装である。

named_values <- values
names(named_values) <-  labels
named_values[target]
#>    eguiqhmukh    yuvlnhfvba    cjbcajffur    nttsfcuszh    ibvgfrjnro 
#> "eguiqhmukh2" "yuvlnhfvba2" "cjbcajffur1" "nttsfcuszh8" "ibvgfrjnro6" 

2. 環境

R言語徹底解説』第8章 (p.160) には、R における環境 (environment) の特別な使用方法としてハッシュマップとして使えると書いてある。 前回の記事ではやや複雑に書いたが、list2env(setNames()) イディオムを使うとやや簡潔に書ける。

hash_env <- list2env(setNames(as.list(values), labels), hash = TRUE)
unlist(mget(target, envir = hash_env))
#>    eguiqhmukh    yuvlnhfvba    cjbcajffur    nttsfcuszh    ibvgfrjnro 
#> "eguiqhmukh2" "yuvlnhfvba2" "cjbcajffur1" "nttsfcuszh8" "ibvgfrjnro6" 

3. hash パッケージ

hash パッケージを使うと直感的で簡潔な記法でハッシュテーブルを扱える。 結果の順序が保証されないことには注意が必要である。

library(hash)
h <- hash(labels, values)
values(h[target])
#>    cjbcajffur    eguiqhmukh    ibvgfrjnro    nttsfcuszh    yuvlnhfvba 
#> "cjbcajffur1" "eguiqhmukh2" "ibvgfrjnro6" "nttsfcuszh8" "yuvlnhfvba2" 

4. collections パッケージ

collections は高速なコンテナデータ型を提供するパッケージであり、ハッシュテーブルは辞書型 Dict として提供される。 メソッドはベクトル化されていないことに注意が必要である。

library(collections)
d <- Dict$new()
invisible(Vectorize(d$set)(labels, values))
Vectorize(d$get)(target)
#>    eguiqhmukh    yuvlnhfvba    cjbcajffur    nttsfcuszh    ibvgfrjnro 
#> "eguiqhmukh2" "yuvlnhfvba2" "cjbcajffur1" "nttsfcuszh8" "ibvgfrjnro6"

5. datastructures

datastructures パッケージはいくつかの高度なデータ型を提供する。 ハッシュテーブルは hashmap で提供される。

library(datastructures)
hm <- hashmap("character")
hm[labels] <- values
unlist(hm[target])
#> [1] "eguiqhmukh2" "yuvlnhfvba2" "cjbcajffur1" "nttsfcuszh8" "ibvgfrjnro6"

6. hashmap パッケージ

hashmap パッケージはキーとバリューに特定の型しか使えないが環境より速いとのこと。

library(hashmap)
H <- hashmap(labels, values)
H[[target]]
#> [1] "eguiqhmukh2" "yuvlnhfvba2" "cjbcajffur1" "nttsfcuszh8" "ibvgfrjnro6"

速度比較

上で紹介した 6 つの方法について速度を比較してみる。 テーブルサイズ N を 100~100000 まで変化させて速度を調べている。

library(magicfor)

magic_for()

for(N in 10^(2:5)) {
  # Create Data -------------------------------------------------------------
  labels <- generate_random_labels(N)
  values <- generate_random_values(labels)
  df <- data.frame(labels, values, stringsAsFactors = FALSE)
  
  target <- sample(labels, 100)
  
  # Preparation -------------------------------------------------------------
  named_values <- values
  names(named_values) <-  labels
  
  hash_env <- list2env(setNames(as.list(values), labels), hash = TRUE)
  
  library(hash)
  h <- hash(labels, values)
  
  library(collections)
  d <- Dict$new()
  invisible(Vectorize(d$set)(labels, values))
  
  library(datastructures)
  hm <- datastructures::hashmap("character")
  hm[labels] <- values
  
  library(hashmap)
  HH <- hashmap::hashmap(labels, values)
  
  # Benchmark ---------------------------------------------------------------
  library(microbenchmark)
  mb_result <- microbenchmark(
    named_vector = {
      named_values[target]
    }, 
    environment = {
      unlist(mget(target, envir = hash_env))
    },
    hash = {
      hash::values(h[target])
    },
    collections = {
      Vectorize(d$get)(target)
    },
    datastructure = {
      unlist(hm[target])
    },
    hashmap = {
      HH[[target]]
    }
  )
  
  time_df <- data.frame(N=as.character(N), mb_result)
  time_df
}

result <- Reduce(rbind, magic_result()$time_df)

結果を可視化する。

library(ggplot2)
ggplot(result, aes(x=N, y=time, fill=expr)) + 
  geom_boxplot() + scale_y_log10() + facet_wrap(~expr)

f:id:hoxo_m:20180828234546p:plain

library(dplyr)
result_mean <- result %>% 
  group_by(N, expr) %>% 
  summarise(mean=mean(time))
ggplot(result_mean, aes(x=N, y=mean, group=expr, color=expr)) + 
  geom_line() + scale_y_log10()

f:id:hoxo_m:20180828234555p:plain

テーブルサイズが数百程度であれば名前付きベクトルが速い。 それ以上は環境が一番速いようである。 hashmap は環境と遜色ない速度であり、直感的で簡潔な記述ができる。 hash パッケージを使う理由はもはや無いようである。

R言語徹底解説

R言語徹底解説

モデルで扱うデータの前処理をrecipesで行う

ドーモ。ホクソエムの @u_ribo です。本業ではモデリングとは離れたギョームをしています。寂しくなったので、Rのrecipesパッケージについて紹介します。

tidymodels.github.io

モデルに適用するデータの前処理

Rでのモデル式 (model formula) の記述って、利用時に不便を感じることや覚えるのが難しい面が時々ありませんか?例えば、y ~ .」は右辺のドットが、目的変数以外の全ての変数を説明変数として扱うことを示しますが、説明変数に対数変換などの変数変換を行うにはy ~ log(.)という記述はできず、結局、説明変数を「+」でつなげていくことになります。また、交互作用項の指定には「x1 * x2」や「(x1 + x2)^2」、「:」を使う表記が可能ですが、この表記には最初は混乱しませんか?(単に私が不勉強なだけということもあります)

加えて、多くのモデルでは欠損への処理が必要となったり、適用するモデルによって(例えば複数の説明変数を扱うK-NNやSVM)は、変数間の標準化が必要です。

ここで紹介するrecipesパッケージを使うことで、こうしたモデル構築に伴うデータの前処理を楽にすることが期待されます。

  • モデルに適用するデータの前処理
  • 対数変換を行うモデルを例に
    • recipesパッケージによるデータの前処理
  • 交互作用を扱うモデルを例に
  • stepに指定可能な処理
  • まとめ
続きを読む

クロネッカー積でデータを列方向(or行方向)に高速に複製もしくは定数倍する

r-wakalangからの転載です。以下のような質問がありました。

data.frameをカラム・ロウ方向に複製結合したdata.frameを出力させたいのですが、どうも綺麗に書けずです。。アドバイスお願いしますm( )m

この意味は、例えば「行方向に2個・列方向に3個複製」の場合は以下の図です。

f:id:StatModeling:20180824174855p:plain

データフレームが数値のみの場合、これは実はクロネッカー積と呼ばれる行列の特殊な積で表現できます。

  • Rで書く場合

Rではデフォルトで%x%関数(or kronecker関数)が用意されているのでそれを使えば簡単に実装できます。

B <- as.matrix(data.frame(a=1:3, b=4:6))
A <- matrix(1, nrow=2, ncol=3)
res <- A %x% B
  • Pythonで書く場合

Pythonでもnumpykronメソッドが用意されていますので、以下のように書くことができます。

import pandas as pd
import numpy as np

B = pd.DataFrame(np.arange(1,7).reshape(2,3).T, columns=list('ab'))
A = pd.DataFrame(np.ones((2,3)))

kp = pd.DataFrame(np.kron(A,B), columns=pd.MultiIndex.from_product([A,B]))

  * * *  

定数倍も行けます。例えば、以下のように

f:id:StatModeling:20180824163606p:plain

左のデータフレームから右のデータフレームが得たい場合です。

  • Rで書く場合
A <- as.matrix(data.frame(a=1:3, b=4:6))
B <- matrix(1:50, nrow=1)
res <- A %x% B
  • Pythonで書く場合
import pandas as pd
import numpy as np

A = pd.DataFrame(np.arange(1,7).reshape(2,3).T, columns=list('ab'))
B = pd.DataFrame(np.arange(1,51).reshape(1,50))

kp = pd.DataFrame(np.kron(A,B), columns=pd.MultiIndex.from_product([A,B]))

Enjoy!

ggplot2 で時系列の区間に影をつけるのは annotate が便利ぽい

例えば次のような時系列データがあるとします。

library(xts)
ts <- as.xts(Nile)

library(ggplot2)
autoplot(ts)

f:id:hoxo_m:20180821233320p:plain

このプロットの 1900年から1940年までの区間に影をつけたい。

これには annotate() が便利ぽい。

autoplot(ts) + 
  annotate("rect", 
           xmin = as.Date("1900-01-01"), 
           xmax = as.Date("1940-01-01"), 
           ymin = -Inf, ymax = Inf, alpha = 0.2)

f:id:hoxo_m:20180821233547p:plain

Enjoy!

参考

RStudioServer から ShinyApp を直接デプロイしたい

現在 CentOS 7.5 サーバーに RStudioServer と ShinyServer を入れて RStuidio 上で ShinyApp を書いています。

デプロイするには /srv/shiny-server/ の下にフォルダを丸ごとコピーしていますが、サーバにいちいちログインするか RStudio の新機能である Terminal 上でコピーしちゃうんですが、これを R でやりたいというのが今日のお題です。

適当に書くと次のようになると思います。

deploy_shiny_app <- function() {
  # 現在のワーキングディレクトリに ShinyApp があるとする
  dir_path <- getwd()
  # ShinyApp のデフォルトのデプロイ場所は /srv/shiny-server/
  target_dir_path <- file.path("/srv/shiny-server", basename(dir_path))
  # ShinyApp のフォルダにある .R ファイルを全て取得
  files <- list.files(dir_path, pattern = "\.R$", full.names = TRUE)
  # sudo cp コマンドで全てコピー
  command <- sprintf("sudo cp -vfu %s %s", paste(files, collapse = " "), 
                     target_dir_path)
  system(command, input = rstudioapi::askForPassword("Enter password"))
}

rstudioapi::askForPassword() というのはコード上にパスワードを平打ちしたくない時にこれ書いとくと RStudio 上でパスワードを入力するプロンプト出してくれるナウいやつです。

さて、これを実行すると次のようなエラーが出ました。

sudo: no tty present and no askpass program specified

これはなんかセキュリティ的な制限で、デフォルトでは sudo 時のパスワードが表示されないようにしてるみたいです。 解除するには

$ sudo visudo

で sudoers ファイルを開いて

Defaults visiblepw

と書くと良いらしいです。

さて、これで実行するとうまくコマンドが実行されるわけですが、デプロイ先のフォルダがないとエラーが出てコピーできません。 なので、フォルダがなければ作るということをやります。

sudo_create_dir_if_not_exists <- function(dir_path) {
  if (!dir.exists(dir_path)) {
    command <- sprintf("sudo mkdir %s", dir_path)
    prompt <- sprintf("Create %s", basename(dir_path))
    system(command, input = rstudioapi::askForPassword(prompt))
  }
}

deploy_shiny_app <- function() {
  # 現在のワーキングディレクトリに ShinyApp があるとする
  dir_path <- getwd()
  # ShinyApp のデフォルトのデプロイ場所は /srv/shiny-server/
  target_dir_path <- file.path("/srv/shiny-server", basename(dir_path))
  # なければ作る
  sudo_create_dir_if_not_exists(target_dir_path)
  # ShinyApp のフォルダにある .R ファイルを全て取得
  files <- list.files(dir_path, pattern = "\.R$", full.names = TRUE)
  # sudo cp コマンドで全てコピー
  command <- sprintf("sudo cp -vfu %s %s", paste(files, collapse = " "),
                     target_dir_path)
  system(command, input = rstudioapi::askForPassword("Enter password"))
}

これでうまくいきそうなもんですが、ShinyApp の静的ファイルを www に入れたりモジュールをサブディレクトリに保存していたりという場合が考えられます。 なので指定したサブディレクトリもコピーするという記述も付け加えます。 このとき、フォルダからフォルダへコピーするという動作は共通なので関数化しちゃいます。

copy_files <- function(dir_path, target_dir_path) {
  # なければ作る
  sudo_create_dir_if_not_exists(target_dir_path)
  # ShinyApp のフォルダにある .R ファイルを全て取得
  files <- list.files(dir_path, pattern = "\\.R$", full.names = TRUE)
  # sudo cp コマンドで全てコピー
  command <- sprintf("sudo cp -vfu %s %s", paste(files, collapse = " "),
                     target_dir_path)
  system(command, input = rstudioapi::askForPassword("Enter password"))
}

deploy_shiny_app <- function(dir_path = getwd(), dest = "/srv/shiny-server") {
  # ShinyApp のデプロイ先
  target_dir_path <- file.path(dest, basename(dir_path))
  # コピーの実行
  copy_files(dir_path, target_dir_path)
}

コピーすべき subdir も指定可能にして for 文で回します。

deploy_shiny_app <- function(dir_path = getwd(), dest = "/srv/shiny-server", 
                             subdir = c("www")) {
  # ShinyApp のデプロイ先
  target_dir_path <- file.path(dest, basename(dir_path))
  # コピーの実行
  copy_files(dir_path, target_dir_path)
  
  # サブディレクトリのコピー
  for (subdir_name in subdir) {
    subdir_path <- file.path(dir_path, subdir_name)
    if (dir.exists(subdir_path)) {
      target_subdir_path <- file.path(target_dir_path, subdir_name)
      copy_files(subdir_path, target_subdir_path)
    }
  }
}

これで多分うまくいきます。 最初デプロイ先のフォルダがない場合はフォルダの作成、サブフォルダの作成とパスワードを何度も聞かれてうざいですが、まあしょうがないかなと。

もっと良い方法があれば教えてください。

Enjoy!

雑記

こんにちは、ホクソエムです。雑記です。

今では当たり前のように使われている pipe演算子こと %>% 。

dplyrパッケージが発表された当初は「気持ち悪い」と評判だったのですが、みなさんもう慣れたのでしょうか。

そしてrlistパッケージの %>>% 。

%>% よりハヤイ!!!!と当時は盛り上がっていましたが誰か使っている方はいるのでしょうか(私は使っていない)。 私が知らないだけで%>>>%とか%>>>>%とかあるのかもしれません。 「私はこんなpipe類似演算子を使っているよ!!!」というご報告お待ちしております。

最後に、pipeを使いたくない方にpipeを使ったコードを従来の表記に戻してくれるパッケージも開発されていましたが、皆さんこちらはご存知でしょうか。

https://github.com/TobCap/demagrittr

どんなものにも歴史はあるもので、流行っているその時にしか味わえない現在性を堪能することも、技術を追いかける醍醐味です。 言語開発の片隅に咲いたそんな一輪の花たちをこれからもお伝えしていきたいと思います。