名前はまだない

データ分析とかの備忘録か, 趣味の話か, はたまた

データ分析の際のデータ品質担保に役立ちそうなRパッケージ【tidylog/assertr/testthat】

はじめに

先週Twitter上でデータ分析の際のテストはどうすべきかって話など、データ分析のデータの品質を確保するための話が話題に上がりました。

業務でやっているのはコードレビューを行いレビューワ環境で同じデータフレームや同じ結果が出力できるかを確認したりするぐらいです。

あとは細かい確認を手動で行うぐらいです。

次のツイートを見かけました。

初めて知ったパッケージでした。

そこで、データ分析におけるデータの品質管理に寄与しそうなパッケージをまとめて積極的に活用していきたいです。

tidylog

github.com

このパッケージはtidyverse系の関数を実行した際に実行ログを表示してくれるようになるパッケージです。

今も使っています。

使い方は非常に簡単で、パッケージを読み込むだけです。

library(tidylog)

この状態でtidyなデータハンドリングを行うと次のようなログが出力されるようになります。

> iris.2 <- iris %>%
+   select(Sepal.Length, Sepal.Width, Species) %>% 
+   filter(Sepal.Width > 2.5) %>% 
+   group_by(Species) %>%
+   summarise(mean = mean(Sepal.Length),
+         sd = sd(Sepal.Length)) 
select: dropped 2 variables (Petal.Length, Petal.Width)
filter: removed 19 rows (13%), 131 rows remaining
group_by: one grouping variable (Species)
summarise: now 3 rows and 3 columns, ungrouped

それぞれの処理では次のログが出力されています。

  • select:どのカラムが落ちたか
  • filter : どの程度のレコードを落としたか
  • group_by:何のカラムでくくったか
  • summarise:いくつのカラムとレコードに集約されたか

ざっとログを確認して想定と異なる処理を行っていないかを確認することができます。

パイプでつなげたないが処理ではどこまで処理が進んでいるのかがわからないことも多いですが、tidylogを使うとで確認できます。

対象となる関数はこちらで確認できます。

https://cran.r-project.org/web/packages/tidylog/tidylog.pdf

参考

assertr

github.com

このassertr パッケージは他の言語のようにプログラムが意図しない動作を行った場合に警告やエラーを出すアサートと同じように、データフレームに対しする処理における意図しない動作がないかを検証し、アラートを出してくれます。

assertr パッケージでは主にverifyとassertとinsistの3つの関数を用います。

verify

verify関数はデータフレームと条件式を引数としてとります。

条件式の結果がFALSEの場合、エラーを出し処理を停止します。

TRUEの場合は、入力されたデータフレームをそのまま出力します。

以下は入力されたデータフレームが30レコード以上あるかを検証した後で、集計を行うコードです。

> mtcars %>%
+   verify(nrow(.) > 30) %>% 
+   summarise(cyl_mean = mean(cyl))
  cyl_mean
1   6.1875

もし、30レコード未満である場合は次のようなエラーが表示され、集計は行われません。

> mtcars %>%
+   slice(1:10) %>% 
+   verify(nrow(.) > 30) %>% 
+   summarise(cyl_mean = mean(cyl))
verification [nrow(.) > 30] failed! (1 failure)

    verb redux_fn    predicate column index value
1 verify       NA nrow(.) > 30     NA     1    NA

 エラー: assertr stopped execution

正負を判断するため、条件式を複数の式で構成しても問題ないです。

mtcars %>%
  verify(nrow(.) > 30 | mpg > 0) %>% 
  summarise(cyl_mean = mean(cyl))

また、has_all_namesやhas_only_names , has_classといった関数と併用することもある。

mtcars %>% 
  verify(has_all_names("mpg", "vs", "am", "wt")) %>% # has_all_namesの引数の名前のカラムがあるか
  verify(has_class("mpg", "wt", class = "numeric"))  # has_classの引数の名前のカラムがclassで指定された型であるか

assert

assert関数は、与えられたデータフレームを次のような関数と共に利用して、想定する状態かを検証します。

  • not_na(): 変数に欠損値が含まれていないか
  • within_bounds(): 変数の値が範囲内に含まれているか
  • in_set(): 数値や文字列が与えた組み合わせから構成されているか

これらの関数は述語関数と呼ばれているようです。

mtcars %>%
  assert(in_set(0,1), am, vs) %>% # amとvsが0か1であるかを検証
  assert(within_bounds(0,5), gear) %>%  # amとvsが0か1であるかを検証
  assert(not_na, wt) # wtがnaかどうか検証

もし、in_setでamとvsが0だけかどうかを検証した場合、次のような出力が行われる。

> mtcars %>%
+   assert(in_set(0), am, vs) 
Column 'am' violates assertion 'in_set(0)' 13 times
    verb redux_fn predicate column index value
1 assert       NA in_set(0)     am     1     1
2 assert       NA in_set(0)     am     2     1
3 assert       NA in_set(0)     am     3     1
4 assert       NA in_set(0)     am    18     1
5 assert       NA in_set(0)     am    19     1
  [omitted 8 rows]

Column 'vs' violates assertion 'in_set(0)' 14 times
    verb redux_fn predicate column index value
1 assert       NA in_set(0)     vs     3     1
2 assert       NA in_set(0)     vs     4     1
3 assert       NA in_set(0)     vs     6     1
4 assert       NA in_set(0)     vs     8     1
5 assert       NA in_set(0)     vs     9     1
  [omitted 9 rows]

また、行に対して検証を行うための関数assert_rowsも用意されている。

mtcars %>%
  assert_rows(num_row_NAs, within_bounds(0,2), everything()) %>% # 全ての行でNAは2つ以下か
  assert_rows(col_concat, is_uniq, mpg, am, wt) %>% # mpg, am, wtの組み合わせがユニークかどうか
  assert_rows(rowSums, within_bounds(0,1), vs:gear) # 各行においてvs+am+gearが0~1かどうかを検証

なお、次のような出力が得られる。

Data frame row reduction 'rowSums' violates predicate 'within_bounds(0, 1)' 32 times
         verb redux_fn           predicate   column index value
1 assert_rows  rowSums within_bounds(0, 1) ~vs:gear     1     5
2 assert_rows  rowSums within_bounds(0, 1) ~vs:gear     2     5
3 assert_rows  rowSums within_bounds(0, 1) ~vs:gear     3     6
4 assert_rows  rowSums within_bounds(0, 1) ~vs:gear     4     4
5 assert_rows  rowSums within_bounds(0, 1) ~vs:gear     5     3
  [omitted 27 rows]


 エラー: assertr stopped execution

また、述語関数は自分で作ることもできる。

カスタム関数は、FALSEを返す必要がある場合のみを指定しなければなりません。

例えば、指定したカラムの文字列が空(0文字)でないことを検証するための関数は次のようになります。

not.empty.p <- function(x) if(x=="") return(FALSE)

insist

多くの場合に検証するカラムのベクトルに基づいて、使用する述語関数を動的に決定する必要があります。

たとえば、ベクトルのすべての要素が平均のn標準偏差内にあるかどうかを確認するには、ベクトル全体を一度読み取って標準偏差を計算することによって境界を動的に決定し、within_boundsによる値域の検証を行う必要があります。

このような処理はassert関数では処理できません。

そのために必要になるのはinsist関数です。

insist関数は、入力された列ベクトルに対して処理を行い利用する述語関数を生成する関数を合わせて利用します。

平均のn標準偏差内かどうかを判別する述語関数を生成する関数はwithin_n_sds関数です。

mpgが平均から3標準偏差以上離れたレコードがないかを検証するのは次のようになります。

mtcars %>%
  insist(within_n_sds(3), mpg) 

もし、2標準偏差以上離れたかの検証した場合、次のような出力になります。

> mtcars %>%
+     insist(within_n_sds(2), mpg) 
Column 'mpg' violates assertion 'within_n_sds(2)' 2 times
    verb redux_fn       predicate column index value
1 insist       NA within_n_sds(2)    mpg    18  32.4
2 insist       NA within_n_sds(2)    mpg    20  33.9

 エラー: assertr stopped execution

また、レコード方向の検証を行うinsist_rows関数もあります。

参考

testthat

github.com

このtestthatはRのコードの単体テストを行うためのパッケージです。

主にexpect_that, test_that, contextの関数を用います。

expect_that(テスト, 正解の条件)
test_that(テストが失敗した時に出すメッセージ,{expect_thatで書かれたテスト})
context(テスト実行時に出すメッセージ)

しかし、contextはver.3.0.0から非推奨とされているようです。

基本的には次のように使用します。

2値から積を計算する関数の出力が想定した値と等しいか検証するには次のようになります。

my_prod <- function(x1,x2) x1*x2

context("work numeric")
test_that("prod function", {
  expect_that(my_prod(1,2), equals(2))
})

# Test passed 🎉

テストが通るとランダムに絵文字が出力されるようです。

テストが通らないと次のようなメッセージが出力されます。

> test_that("prod function", {
+   expect_that(my_prod(1,2), equals(2))
+   expect_that(my_prod(-1,-1), equals(-2))
+ })
─ Failure (Line 3): prod function ───────────────────────
`x` not equal to `expected`.
1/1 mismatches
[1] 1 - -2 == 3
Backtrace:
 1. testthat::expect_that(my_prod(-1, -1), equals(-2))
 2. testthat:::condition(object)
 3. testthat::expect_equal(x, expected, ..., expected.label = label)

expect_thatの正解条件に使える関数は次のようなものがある。 詳細は Chapter 12 Testing | R Packages を見てみてください。

  • expect_equal(x, y):xがyに等しいか(差が10^-7以下程度であれば同値として扱う)

  • expect_identical(x, y):xがyに厳密に等しいか

  • expect_match(s1,s2):文字列s1がs2に一致するか(大文字小文字も判別対象)

  • expect_output(o, s):処理結果oが文字列sに一致するか

  • expect_is(object, s):objectのクラスが文字列sに一致するか

次のようにtest_that内で関数を定義できたりexpect_equalを複数個列挙するような記述することもできます。

test_that("floor_date works for different units", {
  base <- as.POSIXct("2009-08-03 12:01:59.23", tz = "UTC")
  floor_base <- function(unit) lubridate::floor_date(base, unit)
  as_time <- function(x) as.POSIXct(x, tz = "UTC")

  expect_equal(floor_base("second"), as_time("2009-08-03 12:01:59"))
  expect_equal(floor_base("minute"), as_time("2009-08-03 12:01:00"))
  expect_equal(floor_base("hour"),   as_time("2009-08-03 12:00:00"))
  expect_equal(floor_base("day"),    as_time("2009-08-03 00:00:00"))
  expect_equal(floor_base("week"),   as_time("2009-08-02 00:00:00"))
  expect_equal(floor_base("month"),  as_time("2009-08-01 00:00:00"))
  expect_equal(floor_base("year"),   as_time("2009-01-01 00:00:00"))
})

ネットワークに接続していなかったりファイルが存在しなかった場合にテストを停止することができます。

停止することで余分なリソースの消費を防ぐことができます。

これを行うにはskip関数を用います。

skip関数が実行されると引数のメッセージを出力して、以降のテストは実施されずに終了します。

no_connect = TRUE

test_that("skip example", {
  expect_equal(1, 2)  
  if(no_connect)
    skip("No internet connection")
  expect_equal(1, 2)     
  expect_equal(1, 3)    
})

こちらを実行した時の出力は次のようになります。

─ Failure (Line 2): skip example ───────────────────────
1 not equal to 2.
1/1 mismatches
[1] 1 - 2 == -1

─ Skip (Line 3): skip example ─────────────────────────
Reason: No internet connection

ファイルに対してテストを実行することも可能である。

上に記載したmy_prod関数を記述したmy_function.Rを作成し保存しておきます。

そして、次のようなコードのtest/test_sample.Rを作成し保存しました。

library(testthat)
source("../functions/my_function.R")

test_that("prod function #1", {
  expect_that(my_prod(1,2), equals(2))
})

test_that("prod function #2",{
  expect_that(my_prod(-1,-1), equals(-2))
})

ファイルに対してテストを行う場合は次のように実行します。

test_file("test/test_sample.R")

次のような整形された結果が出力されます。

══ Testing test_sample.R ══════════════════════ 
[ FAIL 1 | WARN 0 | SKIP 0 | PASS 1 ]

─ Failure (test_sample.R:10:3): prod function #2 ───────────────
`x` not equal to `expected`.
1/1 mismatches
[1] 1 - -2 == 3
Backtrace:
 1. testthat::expect_that(my_prod(-1, -1), equals(-2)) test_sample.R:10:2
 2. testthat:::condition(object)
 3. testthat::expect_equal(x, expected, ..., expected.label = label)

[ FAIL 1 | WARN 0 | SKIP 0 | PASS 1 ]

参考

その他

機械学習系になるとtidymodelsやrecipes(tidymodelに含まれる)を利用するのが良い気がします。

github.com

github.com

最初はとっつきにくいなと思いましたが、慣れれば恩恵をすごい感じます。

同じような回帰モデルを複数回利用するならbroom(こちらもtidymodelに含まれますが)とかが良い気がいます。

github.com

データフレームの差を見るにはcompareDFを使うのがいいんじゃないかなと思います。

github.com

最後に

他にも良いpackageや方法論があれば紹介してほしいです。

あと、ちゃんとTDDを勉強資料と思います。