Tidyevalでの関数型プログラミング俺俺メモ
プログラミングをするときにはDRYの法則と言われるように、同じような処理は関数で書いて使い回す方がメンテ効率や可読性が上がる。
特に、データサイエンス的な場合はコードの使い捨てがしやすいので煩雑になるので特に意識した方が良い。
一方で、Rのtidyverse
は超便利だけど、関数を中心とした関数型プログラミング的な書き方、つまり変数を用いて色々変えつつやるときにNSEとかが絡んできて面倒。
過去にtidyverseとNSEについて書いたが、結局使うときにちゃんとできてないなぁといった感じなのでそれぞれでの書き方をまとめる。
なお、そのようなtidyeval
という概念の詳細は過去記事に書いている。
要点として、tidyverse系は通常のSE表現(Standard Evaluation)ではなく、NSE表現(Non-Standard Evaluation)を使っているので表現式(expression)で書こう、という話。
全体的な話は公式ガイドを読むとよさげ
書き方系
!!
!!
をQuosure
に与えるとその箇所はquote
ではなくなる。つまり、評価がされるようになる。なお、一部だけに適応したい場合は()
で囲む
quo, enquo
quo
, unquo
はQuosure
を作成する。また、!!
と組み合わせることで、遅延評価が可能となる。例えば、
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
内で初めて評価がおこなわれ、そのときはdf
でSpecies
は定義されているので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
を使う。
なお、文字列で定義したい場合は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
:=
mutate
やfileter
の左側の列名に!!
を用いて展開した値を使用する場合は、右側とを=で繋ぐのではなく:=をセットで用いる必要がある。
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
各関数での使い方サンプル
各関数で実際どう書くか羅列。
前述の公式ガイドにクックブックもある。
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)