読者です 読者をやめる 読者になる 読者になる

dplyrのmutate_if()とかについて

dplyr R

1か月前にキターとつぶやいたものがついにCRANにきたのでそれについて書きます。

これは何なのか

複数のカラムに対して同じ処理をするとき、これまではmutate_each()summarise_each()という関数がありました。

たとえば、Sepalから始まるカラムすべてにmin()max()を適用したいときはこんな感じです。

library(dplyr)

iris %>%
  group_by(Species) %>%
  summarise_each(funs(min, max), starts_with("Sepal"))
#> Source: local data frame [3 x 5]
#> 
#>      Species Sepal.Length_min Sepal.Width_min Sepal.Length_max Sepal.Width_max
#>       <fctr>            <dbl>           <dbl>            <dbl>           <dbl>
#> 1     setosa              4.3             2.3              5.8             4.4
#> 2 versicolor              4.9             2.0              7.0             3.4
#> 3  virginica              4.9             2.2              7.9             3.8

これまで、mutate_each()summarise_each()は以下のことができました

  1. すべてのカラムに関数を適用する
  2. カラム名が条件にマッチしたカラムに関数を適用する

しかし例えば、

  • factor型のカラムをすべてcharacter型に変換する
  • 数値だったらround()で小数点を丸める

みたいな、カラムの中身に条件をつけることはできませんでした。

それ、purrrならできます

このIssueに報告している通りですが、実はpurrrパッケージなら、そういうのができるmap_if()という関数があります。

ただ、purrrはlistを念頭に置いているのでちょっと不格好になります。

library(purrr)

as.list(iris) %>%
  map_if(is.numeric, max)
#> $Sepal.Length
#> [1] 7.9
#> 
#> $Sepal.Width
#> [1] 4.4
#> 
#> $Petal.Length
#> [1] 6.9
#> 
#> $Petal.Width
#> [1] 2.5
#> 
#> $Species
#>   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa    
#>  [10] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
#> ...

なぜas.list()しているのかというと、data.frameのままだと関数が行方向に適用されてしまうからです。しかし、これをもういちどdata.frameに直そうと思うとめんどくさいですよね…

dplyrでもっとスマートにできるようになりました

でももうそんなめんどくさいことをする必要はありません。dplyrでできるようになりました。

iris %>%
  summarise_if(is.numeric, funs(max))
#>   Sepal.Length Sepal.Width Petal.Length Petal.Width
#> 1          7.9         4.4          6.9         2.5

mutate_each()はやがて廃止されて、次のような使い分けになります。

  1. すべてのカラムに関数を適用する:mutate_all()summarise_all()
  2. カラム名が条件にマッチしたカラムに関数を適用する:mutate_at()summarise_at()
  3. カラムの内容が条件にマッチしたカラムに関数を適用する:mutate_if()summarise_if()(、select_if()

ちょっと使ってみます。

※関数の指定の仕方はfuns()で包む以外にもいくつかあります。気になると思いますが後回しにしてひとまずこの3つについて書きます。

すべてのカラムに関数を適用する

これは超単純です。mutate_all()summarise_all()はすべてのカラムに関数を適用します。

data.frame(a = 1:10, b = 11:20) %>%
  summarise_all(funs(min, max))
#>   a_min b_min a_max b_max
#> 1     1    11    10    20

カラム名が条件にマッチしたカラムに関数を適用する

mutate_at()summarise_at()は指定した位置のカラムに関数を適用します。カラムは数字または文字列で指定します。

iris %>%
  group_by(Species) %>%
  summarise_at(c(1, 2),
               funs(min, max))
#>      Species Sepal.Length_min Sepal.Width_min Sepal.Length_max Sepal.Width_max
#>       <fctr>            <dbl>           <dbl>            <dbl>           <dbl>
#> 1     setosa              4.3             2.3              5.8             4.4
#> 2 versicolor              4.9             2.0              7.0             3.4
#> 3  virginica              4.9             2.2              7.9             3.8

select()の中で使える関数は(?select_helpersとやると一覧が見れます)、条件に当てはまるカラムのインデックスを返してくれるので組み合わせると便利です。

starts_with("Sepal", vars = colnames(iris))
#> [1] 1 2

select_helpersな関数を使うときはvars()の中に書きます。たとえば、冒頭の例で出したSepalから始まるカラムすべてにmin()max()を適用したいときはこんな感じです。

iris %>%
  group_by(Species) %>%
  summarise_at(vars(starts_with("Sepal")),
               funs(min, max))
#>      Species Sepal.Length_min Sepal.Width_min Sepal.Length_max Sepal.Width_max
#>       <fctr>            <dbl>           <dbl>            <dbl>           <dbl>
#> 1     setosa              4.3             2.3              5.8             4.4
#> 2 versicolor              4.9             2.0              7.0             3.4
#> 3  virginica              4.9             2.2              7.9             3.8

mutate_each()と比べるとvars()を使わないといけない分だけちょっとめんどくさくなりました。でもその分、次の指定の仕方もできるようになりました。

カラムの内容が条件にマッチしたカラムに関数を適用する

mutate_if()summarise_if()は、predicate(叙述関数)に当てはまるカラムにのみ関数を適用します。たとえば、「factor型のカラムをすべてcharacter型に変換する」というのはこう書けます。

iris %>%
  mutate_if(is.factor, funs(as.character)) %>%
  glimpse
#> Observations: 150
#> Variables: 5
#> $ Sepal.Length (dbl) 5.1, 4.9, 4.7, 4.6, 5.0, 5.4, 4.6, 5.0, 4.4, 4.9, 5.4, 4.8, 4.8, 4.3, 5.8, 5.7, 5...
#> $ Sepal.Width  (dbl) 3.5, 3.0, 3.2, 3.1, 3.6, 3.9, 3.4, 3.4, 2.9, 3.1, 3.7, 3.4, 3.0, 3.0, 4.0, 4.4, 3...
#> $ Petal.Length (dbl) 1.4, 1.4, 1.3, 1.5, 1.4, 1.7, 1.4, 1.5, 1.4, 1.5, 1.5, 1.6, 1.4, 1.1, 1.2, 1.5, 1...
#> $ Petal.Width  (dbl) 0.2, 0.2, 0.2, 0.2, 0.2, 0.4, 0.3, 0.2, 0.2, 0.1, 0.2, 0.2, 0.1, 0.1, 0.2, 0.4, 0...
#> $ Species      (chr) "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "...

分かりづらいですが、Speciesのみにas.characterが適用されています。

predicateは自分で作った関数も指定できます。たとえば、数値でかつ平均が3.5以上のカラムのmax()、というのは以下のように書けます。

iris %>%
  summarise_if(function(x) is.numeric(x) & mean(x) > 3.5, funs(max))
#>   Sepal.Length Petal.Length
#> 1          7.9          6.9

便利そう!

ちなみにselect()でも同じくselect_if()というのが用意されています。

関数の指定の仕方は色々

関数の指定の仕方はいくつかあります。ヘルプに載ってたのはこんな感じ

g <- group_by(iris, Species)

# ひとつだけならそのまま指定できる
g %>% summarise_all(max)

# 二つ以上を並べるならfuns()でくるむか文字列のベクトルにする
g %>% summarise_all(funs(min, max))
g %>% summarise_all(c("min", "max"))

# カラム名を指定したいときもfuns()でくるむか文字列のベクトルにする
g %>% summarise_all(funs(最大 = max))
g %>% summarise_all(c(最大 = "max"))

# . を使うとラムダ関数的に使える
g %>% mutate_all(funs(. * 0.4))

# 共通の引数は関数に続けて指定する
g %>% summarise_all(mean, trim = 1)

分かりづらい挙動&バグも色々...

こんな感じで便利そうではあるんですが、なんかところどころ挙動が変です。

例えば、上に書いたようにsummarise_at()starts_with()を使うときはvars()に囲まないとダメなんですが、 うっかり囲み忘れても動きます(!)

iris %>%
  group_by(Species) %>%
  summarise_at(starts_with("Sepal"),
               funs(min, max)) %>%
  colnames()
#> [1] "Species"          "Sepal.Length_min" "Sepal.Width_min"  "Petal.Length_min" "Petal.Width_min" 
#> [6] "Sepal.Length_max" "Sepal.Width_max"  "Petal.Length_max" "Petal.Width_max" 

全部入っちゃってますね…

あと、羽鳥が思い違いをしていたせいでselect()の挙動が変わってしまっているみたいです。注意してください。

自作select helper関数

上の挙動を調べてるときに気づいたんですが、vars()の中やselect()の中で指定する関数は、以下の条件を満たすものをつくればいいみたいです。

  • current_vars()カラム名を受け取る
  • マッチしたカラムのインデックスを返す

例えば、Aというカラムにだけヒットする関数は以下のように実装できます。

f <- function() which(current_vars() == "A")
data.frame(A = 1:3, b = 1:3, c = 1:3) %>% select(f())
#>     A
#> 1   1
#> 2   2
#> 3   3

感想

しばらくは、バグなのか仕様なのかよく分からない動作に苦しむ人を見かけそうな予感がします… 困ったら、ひとりで悩まずお近くのホクソエムまたはr-wakalangまでご相談ください!