dplyrのVectorized Functionsについて

  • 7
    いいね
  • 0
    コメント

最近いろんなのが出てきて追いきれてなかったのでメモ。

Vectorized Functionsとは

dplyrにはmutate()transmute()など,新たな変数を作成する関数があります。多くの場合,元ある変数から計算(操作)して新たな変数を導くのですが,この計算(操作)に使える便利な関数たちをVectorized Funcitonsと呼ぶようです。

これはRStudioが公開しているチートシート群の中の,Data Transformation Cheat Sheetの2ページ目に一覧があります1

今回はここに紹介してある関数を実際にテストしてみます。

関数一覧

チートシートの該当箇所にある関数は以下のとおりです:

  • オフセット(Offsets)
    • dplyr::lag()
    • dplyr::lead()
  • 累積集計(Cumulative Aggregates)
    • dplyr::cumall()
    • dplyr::cumany()
    • cummax()
    • cummin()
    • cumprod()
    • cumsum()
  • ランキング(Rankings)
    • dplyr::cume_dist()
    • dplyr::dense_rank()
    • dplyr::min_rank()
    • dplyr::ntile()
    • dplyr::percent_rank()
    • dplyr::row_number()
  • Math
    • +, -, *, /, %/%, %%
    • log(), log2(), log10()
    • <, <=, >, >=, !=, ==
  • その他(Misc)
    • dplyr::between()
    • dplyr::case_when()
    • dplyr::coalesce()
    • dplyr::if_else()
    • dplyr::na_if()
    • pmax()
    • pmin()
    • dplyr::recode()
    • dplyr::recode_factor()

ただこれらのうち,オフセットと累積,そしてランキングはいわゆるWindow関数と呼ばれるもので,すでに非常にわかりやすい解説記事が以下にあります:

dplyrを使いこなす!Window関数編 - Qiita

なので,これらについては終わりの方に参照リンクを紹介します2。また,Mathについては説明を省略します。従って,結局「その他」に区分されている関数について,実際にテストしていきます。

以下,この後使うパッケージとデータセットを準備します:

library(dplyr)
#>  
#>  Attaching package: 'dplyr'
#>  The following objects are masked from 'package:stats':
#>  
#>      filter, lag
#>  The following objects are masked from 'package:base':
#>  
#>      intersect, setdiff, setequal, union

x <- data.frame(
  v1 = 1:6,
  v2 = sample(1:2, 6, replace = TRUE),
  v3 = LETTERS[1:6],
  v4 = sample(LETTERS[1:2], 6, replace = TRUE),
  v5 = c(1:3, NA, 5:6)
)

glimpse(x)
#>  Observations: 6
#>  Variables: 5
#>  $ v1 <int> 1, 2, 3, 4, 5, 6
#>  $ v2 <int> 2, 1, 1, 1, 2, 2
#>  $ v3 <fctr> A, B, C, D, E, F
#>  $ v4 <fctr> B, A, B, B, A, B
#>  $ v5 <int> 1, 2, 3, NA, 5, 6

between()

範囲を指定し,そこに収まるのであればTRUEを,そうでなければFALSEを返します:

x %>% 
  select(v1) %>% 
  mutate(ho = between(v1, 2, 4))
#>    v1    ho
#>  1  1 FALSE
#>  2  2  TRUE
#>  3  3  TRUE
#>  4  4  TRUE
#>  5  5 FALSE
#>  6  6 FALSE

第一引数(x)が対象となる数値ベクトル(変数名),第二引数(left)が下限,第三引数(right)が上限となります。なお上限と下限に指定した値を含みます(つまりは>=<=)。

どちらかというとfilter()の中で使う場面が多いかと思います。

if_else()

baseのifelse()と同様ですが,こちらのほうがより厳密で,trueの時の処理内容とfalseの時の処理内容をチェックして,同じ型で同じclassかどうかをみるようです。あとこちらには引数にmissingがあり,欠損値への処理が可能です。

x %>% 
  select(v5) %>% 
  mutate(ho = if_else(v5 > 3, NA_integer_, v5),
         fu = if_else(v5 %% 3 == 0, "fizz", ""), 
         pi = if_else(v5 > 5, "high", "low", "missing" ))
#>    v5 ho   fu      pi
#>  1  1  1          low
#>  2  2  2          low
#>  3  3  3 fizz     low
#>  4 NA NA <NA> missing
#>  5  5 NA          low
#>  6  6 NA fizz    high

coalesce()

「指定したベクトルのセットから,各行で欠損じゃない最初の値を見つけてきて持ってくる」という仕事をします。SQLのCOALESCE関数からインスパイアされたものだそうです:

x %>% 
  select(v5, v2) %>% 
  mutate(ho = coalesce(v5, v2))
#>    v5 v2 ho
#>  1  1  2  1
#>  2  2  1  2
#>  3  3  1  3
#>  4 NA  1  1
#>  5  5  2  5
#>  6  6  2  6

このコードの場合,v5には4行目にNAがあります。そこでセットにされているベクトル(v5とv4)で,左から順にNAではない値を探しに行き,ヒットした値を持ってきます。

na_if()

条件に合致した値をNAにします:

x %>% 
  select(v1, v3) %>% 
  mutate(
    ho = na_if(v1, 3),
    fu = na_if(v3, "D"),
    pi = na_if(v1, rep(1:3, 2))
  )
#>    v1 v3 ho   fu pi
#>  1  1  A  1    A NA
#>  2  2  B  2    B NA
#>  3  3  C NA    C NA
#>  4  4  D  4 <NA>  4
#>  5  5  E  5    E  5
#>  6  6  F  6    F  6

第二引数には単一の値,もしくは第一引数と同一の長さのベクトルを指定します。

pmax(), pmin()

並行比較して,最大(or最小)最小を返してきます:

x %>% 
  select(v1, v2) %>% 
  mutate(
    ho = pmax(v1, 6:1),
    fu = pmin(v1, 6:1),
    pi = pmax(v1, v2, 4),
    ga = pmin(v1, v2, 4)
  )
#>    v1 v2 ho fu pi ga
#>  1  1  2  6  1  4  1
#>  2  2  1  5  2  4  1
#>  3  3  1  4  3  4  1
#>  4  4  1  4  3  4  1
#>  5  5  2  5  2  5  2
#>  6  6  2  6  1  6  2

要するに,引数で指定したベクトルを束ね,横方向で見ていって最大(or最小)の値を持ってくる,という仕事をします。ベクトルの長さが合わない場合は要素を再利用することに注意してください。このあたりの挙動はbaseの関数って感じです。

recode(), recode_factor()

該当する値を書き換えます:

x %>% 
  select(v5, v3) %>% 
  mutate(
    ho = recode(v3, A = "kosaki"),
    fu = recode(v3, A = "kosaki", .default = "xxx"),
    pi = recode(v5, `5` = 55L, `6` = 66L, .default = v5),
    ga = recode(v5, `5` = 55L, `6` = 66L, .default = v5, .missing = -1L)
  )
#>    v5 v3     ho     fu pi ga
#>  1  1  A kosaki kosaki  1  1
#>  2  2  B      B    xxx  2  2
#>  3  3  C      C    xxx  3  3
#>  4 NA  D      D    xxx NA -1
#>  5  5  E      E    xxx 55 55
#>  6  6  F      F    xxx 66 66

見てのとおりです。なお,factor型向けにrecode_factor()があるのですが,関数のヘルプドキュメントにあるとおりforcatsパッケージを使っていったほうがいいでしょう。

case_when()

if_else()の複数ケースで,まとめてできます:

x %>% 
  select(v1, v4) %>% 
  mutate(
    ho = case_when(
      v1 %% 2 == 0 ~ "Even",
      v1 %% 2 == 1 ~ "Odd"
    ),
    fu = case_when(
      v1 <= 2 ~ v1 * -1,
      between(v1, 3, 5) ~ as.numeric(v1),
      TRUE ~ v1 ^ 2
    ),
    pi = case_when(
      v4 == "A" ~ "kosaki"
    ),
    yo = case_when(
      v4 == "A" ~ "kosaki",
      TRUE ~ as.character(v4)      
    ),
    ga = case_when(
      TRUE ~ as.character(v4),
      v4 == "A" ~ "kosaki"
    )
  )
#>    v1 v4   ho fu     pi     yo ga
#>  1  1  B  Odd -1   <NA>      B  B
#>  2  2  A Even -2 kosaki kosaki  A
#>  3  3  B  Odd  3   <NA>      B  B
#>  4  4  B Even  4   <NA>      B  B
#>  5  5  A  Odd  5 kosaki kosaki  A
#>  6  6  B Even 36   <NA>      B  B

case_when()では複数の条件を指定していくことができ,引数に論理式と置き換える値を記述していきます。この際,~を利用したformulaを使用します。

また,論理式で拾えなかった要素については全てNAが返ってきます。なお,「それ以外」を指し示すのはTRUEで,上記のように~の左辺に記述すればOKです。

注意事項ですが,順番が重要で最初に書いてあるものが優先されます。yoとgaのコードと出力を比較してください。

オフセット関数,累積関数,ランキング関数について

先に紹介した記事にわかりやすい解説がありますので,そちらをぜひ参照してください:

雑感

ここで実際に取り上げた関数はすごく便利なものもあれば,使いどころが少ないけどいい仕事をするものもあったりして,できればおさておきたいところです。

mutateはデータハンドリングでもかなりポイントになるので,これらの関数についてはできるだけさくっと使えるようになりたいですね。

Enjoy!


  1. このチートシート群ですが,頻繁に更新されています。ちょっと前まではdplyr&tidyrだったのですが,区分すら変わってしまってます… 

  2. こちらの記事には,さらにローリング関数についても解説してあります。丁寧でわかりやすい説明があり,本当に感謝です。