まずは蝋の翼から。

学んだことを書きながら確認・整理するためのメモブログ。こういうことなのかな?といったことをふわっと書いたりしていますが、理解が浅いゆえに的はずれなことも多々あると思うのでツッコミ歓迎

Tidyevalでの関数型プログラミング俺俺メモ

プログラミングをするときにはDRYの法則と言われるように、同じような処理は関数で書いて使い回す方がメンテ効率や可読性が上がる。
特に、データサイエンス的な場合はコードの使い捨てがしやすいので煩雑になるので特に意識した方が良い。

zerebom.hatenablog.com

一方で、Rのtidyverseは超便利だけど、関数を中心とした関数型プログラミング的な書き方、つまり変数を用いて色々変えつつやるときにNSEとかが絡んできて面倒。

過去にtidyverseとNSEについて書いたが、結局使うときにちゃんとできてないなぁといった感じなのでそれぞれでの書き方をまとめる。

なお、そのようなtidyevalという概念の詳細は過去記事に書いている。

knknkn.hatenablog.com

要点として、tidyverse系は通常のSE表現(Standard Evaluation)ではなく、NSE表現(Non-Standard Evaluation)を使っているので表現式(expression)で書こう、という話。

全体的な話は公式ガイドを読むとよさげ

tidyeval.tidyverse.org

書き方系

!!

!!Quosureに与えるとその箇所はquoteではなくなる。つまり、評価がされるようになる。なお、一部だけに適応したい場合は()で囲む

quo, enquo

quo, unquoQuosureを作成する。また、!!と組み合わせることで、遅延評価が可能となる。例えば、

library(tidyverse)

df = as_tibble(iris)

df %>% 
  filter((!!hoge) == 'versicolor') %>% ## hogeというquoteとして渡したあと、!!でquoteを一時的に外して評価される
  head()

と書くと、hoge!!によって評価されるようになるのでhogeに何かを代入しておくと代入された項目が使える。
しかし、

hoge = Species

df %>% 
  filter((!!hoge) == 'versicolor') %>% ## hogeというquoteとして渡したあと、!!でquoteを一時的に外して評価される
  head()

とすると、Speciesというオブジェクトが代入時に評価されるが、Speciesはこの段階では定義されてないのでエラーとなる。また、ここを仮にhoge = 'Species'とした場合、文字列Speciesとして定義されているのでエラーにはならないがNSEでは以下のように、Species列ではなく文字列Species列となり、そんな列は存在しないので0行抽出される。

df %>% 
  filter('Species' == 'versicolor') %>% ## hogeというquoteとして渡したあと、!!でquoteを一時的に外して評価される
  head()

# A tibble: 0 x 5
# … with 5 variables: Sepal.Length <dbl>, Sepal.Width <dbl>, Petal.Length <dbl>, Petal.Width <dbl>, Species <fct>

このとき、前述のようにquoで遅延評価にしておくとfilter内で初めて評価がおこなわれ、そのときはdfSpeciesは定義されているのでdfでの定義を用いてデータを呼び出すことになる。

# 変数hoge
hoge = quo(Species) # Speciesを遅延評価

df %>% 
  filter((!!hoge) == 'versicolor') %>% ## hogeというquoteとして渡したあと、!!でquoteを一時的に外して評価される
  head()
## A tibble: 6 x 5
# Sepal.Length Sepal.Width Petal.Length Petal.Width Species   
# <dbl>       <dbl>        <dbl>       <dbl> <fct>     
#   1          7           3.2          4.7         1.4 versicolor
# 2          6.4         3.2          4.5         1.5 versicolor
# 3          6.9         3.1          4.9         1.5 versicolor
# 4          5.5         2.3          4           1.3 versicolor
# 5          6.5         2.8          4.6         1.5 versicolor
# 6          5.7         2.8          4.5         1.3 versicolor

他にも、enquoというものがあり、シンボル(変数とか引数とか)に対するquo

厳密には、遅延評価なのは同じだが、quoは環境がlocal、unquoは環境がglobalとなる。

つまり、関数内で使用する場合、
quo()では実行されているその環境を用いるので、渡された引数をそのままexprに設定する。 enquo()では関数の呼び出し元の環境を用いるので、その呼び出し元の環境で引数を評価したものをexprに設定する。

そのため、関数の引数を使う場合はquoではなくunquoを使う。

ronri-rukeichi.hatenablog.com

なお、文字列で定義したい場合はsym で一度シンボル化する。

piyo = sym('Species') # シンボル化
# piyo = as.name('Species') # こっちでもよい
hoge = enquo(piyo) # Speciesを遅延評価

df %>% 
  filter((!!hoge) == 'versicolor') %>% ## hogeというquoteとして渡したあと、!!でquoteを一時的に外して評価される
  head()
## A tibble: 6 x 5
# Sepal.Length Sepal.Width Petal.Length Petal.Width Species   
# <dbl>       <dbl>        <dbl>       <dbl> <fct>     
#   1          7           3.2          4.7         1.4 versicolor
# 2          6.4         3.2          4.5         1.5 versicolor
# 3          6.9         3.1          4.9         1.5 versicolor
# 4          5.5         2.3          4           1.3 versicolor
# 5          6.5         2.8          4.6         1.5 versicolor
# 6          5.7         2.8          4.5         1.3 versicolor

:=

mutatefileterの左側の列名に!!を用いて展開した値を使用する場合は、右側とを=で繋ぐのではなく:=をセットで用いる必要がある。

tmp_hoge = as.name('hoge') # シンボル化
hoge = enquo(tmp_hoge) # Speciesを遅延評価

df %>% 
  mutate((!!hoge) := Species) %>% ## hogeというquoteとして渡したあと、!!でquoteを一時的に外して評価される
  head()
# # A tibble: 6 x 6
# Sepal.Length Sepal.Width Petal.Length Petal.Width Species hoge  
# <dbl>       <dbl>        <dbl>       <dbl> <fct>   <fct> 
#   1          5.1         3.5          1.4         0.2 setosa  setosa
# 2          4.9         3            1.4         0.2 setosa  setosa
# 3          4.7         3.2          1.3         0.2 setosa  setosa
# 4          4.6         3.1          1.5         0.2 setosa  setosa
# 5          5           3.6          1.4         0.2 setosa  setosa
# 6          5.4         3.9          1.7         0.4 setosa  setosa


# 同じ結果
hoge = quo(hoge) # Speciesを遅延評価

df %>% 
  mutate((!!hoge) := Species) %>% ## hogeというquoteとして渡したあと、!!でquoteを一時的に外して評価される
  head()
# # A tibble: 6 x 6
# Sepal.Length Sepal.Width Petal.Length Petal.Width Species hoge  
# <dbl>       <dbl>        <dbl>       <dbl> <fct>   <fct> 
#   1          5.1         3.5          1.4         0.2 setosa  setosa
# 2          4.9         3            1.4         0.2 setosa  setosa
# 3          4.7         3.2          1.3         0.2 setosa  setosa
# 4          4.6         3.1          1.5         0.2 setosa  setosa
# 5          5           3.6          1.4         0.2 setosa  setosa
# 6          5.4         3.9          1.7         0.4 setosa  setosa

各関数での使い方サンプル

各関数で実際どう書くか羅列。

前述の公式ガイドにクックブックもある。

tidyeval.tidyverse.org

mutate

f_mutate = function(df, mutate_col, col1, col2) {
  mutate_col = enquo(mutate_col)
  col1 = enquo(col1)
  col2 = enquo(col2)
  
  df = df %>% 
    mutate((!!mutate_col) := (!!col1) * (!!col2))
  
  return(df)
  
}

f_mutate(df, col_by, Sepal.Length, Sepal.Width)

# # A tibble: 150 x 6
# Sepal.Length Sepal.Width Petal.Length Petal.Width Species col_by
# <dbl>       <dbl>        <dbl>       <dbl> <fct>    <dbl>
#   1          5.1         3.5          1.4         0.2 setosa    17.8
# 2          4.9         3            1.4         0.2 setosa    14.7
# 3          4.7         3.2          1.3         0.2 setosa    15.0
# 4          4.6         3.1          1.5         0.2 setosa    14.3
# 5          5           3.6          1.4         0.2 setosa    18  
# 6          5.4         3.9          1.7         0.4 setosa    21.1
# 7          4.6         3.4          1.4         0.3 setosa    15.6
# 8          5           3.4          1.5         0.2 setosa    17  
# 9          4.4         2.9          1.4         0.2 setosa    12.8
# 10          4.9         3.1          1.5         0.1 setosa    15.2
# # … with 140 more rows

filter

f_filter = function(df, filtered_col, filtered_val) {
  filtered_col = enquo(filtered_col)
  
  df = df %>% 
    filter((!!filtered_col) == filtered_val)
  
  return(df)
  
}

f_filter(df, Species, 'setosa')

# # A tibble: 50 x 5
# Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# <dbl>       <dbl>        <dbl>       <dbl> <fct>  
#   1          5.1         3.5          1.4         0.2 setosa 
# 2          4.9         3            1.4         0.2 setosa 
# 3          4.7         3.2          1.3         0.2 setosa 
# 4          4.6         3.1          1.5         0.2 setosa 
# 5          5           3.6          1.4         0.2 setosa 
# 6          5.4         3.9          1.7         0.4 setosa 
# 7          4.6         3.4          1.4         0.3 setosa 
# 8          5           3.4          1.5         0.2 setosa 
# 9          4.4         2.9          1.4         0.2 setosa 
# 10          4.9         3.1          1.5         0.1 setosa 

select

f_select = function(df, col1, col2) {
  col1 = enquo(col1)
  col2 = enquo(col2)
  
  df = df %>% 
    select((!!col1), (!!col2))
  
  return(df)
  
}

f_select(df, Sepal.Length, Sepal.Width)

# # A tibble: 150 x 2
# Sepal.Length Sepal.Width
# <dbl>       <dbl>
#   1          5.1         3.5
# 2          4.9         3  
# 3          4.7         3.2
# 4          4.6         3.1
# 5          5           3.6
# 6          5.4         3.9
# 7          4.6         3.4
# 8          5           3.4
# 9          4.4         2.9
# 10          4.9         3.1
# # … with 140 more rows

group_by, summarise

f_summarise = function(df, group_col, summarise_col) {
  group_col = enquo(group_col)
  summarise_col = enquo(summarise_col)
  
  df = df %>% 
    group_by(!!group_col) %>% 
    summarise((!!summarise_col) := mean((!!summarise_col)))
  
  return(df)
  
}

f_summarise(df, Species, Sepal.Length)

# # A tibble: 3 x 2
# Species    Sepal.Length
# * <fct>             <dbl>
#   1 setosa             5.01
# 2 versicolor         5.94
# 3 virginica          6.59

ggplot

f_plot = function(df, var_x) {
  var_x = enquo(var_x)
  
  g = df %>% 
    ggplot(aes((!!var_x), Sepal.Width)) +
    geom_point()
  
  return(g)
  
}

f_plot(df, Sepal.Width)

おまけ

expr関数を使うと、今コードが最終的にどういう状態になっているか確認ができる。

mutate

f_mutate = function(df, mutate_col, col1, col2) {
  mutate_col = enquo(mutate_col)
  col1 = enquo(col1)
  col2 = enquo(col2)
  
  df = df %>% 
    mutate((!!mutate_col) := (!!col1) * (!!col2))
  
  return(df)
  
}

f_mutate(df, col_by, Sepal.Length, Sepal.Width)

# f_mutateでなにが起きているか確認
expr_f_mutate = function(df, mutate_col, col1, col2) {
  mutate_col = enquo(mutate_col)
  col1 = enquo(col1)
  col2 = enquo(col2)
  
  expr_f = expr(df %>% 
    mutate((!!mutate_col) := (!!col1) * (!!col2)))
  
  return(expr_f)
  
}

expr_f_mutate(df, col_by, Sepal.Length, Sepal.Width)
# df %>% mutate(`:=`(~col_by, (~Sepal.Length) * ~Sepal.Width))

select

f_select = function(df, col1, col2) {
  col1 = enquo(col1)
  col2 = enquo(col2)
  
  df = df %>% 
    select((!!col1), (!!col2))
  
  return(df)
  
}

f_select(df, Sepal.Length, Sepal.Width)

# # A tibble: 150 x 2
# Sepal.Length Sepal.Width
# <dbl>       <dbl>
#   1          5.1         3.5
# 2          4.9         3  
# 3          4.7         3.2
# 4          4.6         3.1
# 5          5           3.6
# 6          5.4         3.9
# 7          4.6         3.4
# 8          5           3.4
# 9          4.4         2.9
# 10          4.9         3.1
# # … with 140 more rows


# f_selectでなにが起きているか確認
expr_f_select = function(df, col1, col2) {
  col1 = enquo(col1)
  col2 = enquo(col2)
  
  expr_df = expr(df %>% 
    select((!!col1), (!!col2)))
  
  return(expr_df)
  
}

expr_f_select(df, Sepal.Length, Sepal.Width)
# => df %>% select(~Sepal.Length, ~Sepal.Width)

参考

speakerdeck.com

qiita.com