Rのtidyverse圏で関数をうまく作る

2022/05/20に公開

モチベーション

Rでデータハンドリングするときは、tidyverseを使うことが多いです。
また、医学系研究では、繰り返し測定したある変数を、縦持ちではなく、横持ちでデータセットに格納されていることはよくあります。

一つの変数だけならpivot_longer()を一度使えば良いのですが、同じような処理をする変数がたくさんあると、関数作りたいですよね。

今回(というか毎回)、この関数作りでつまずくので、(自分のためにも)まとめてみました。

達成したいこと

下のデータセットの、繰り返し測定した変数Xに対して、

こうしたい。

そして、Xに対してもYに対しても、同じ処理をしたいため、関数にしたい。

データの仕様

横持ちデータ

変数 内容
id 個体識別コード
X0m 変数Xの0ヶ月目(ベースライン)の値
X3m 変数Xの3ヶ月目の値
X6m 変数Xの6ヶ月目の値
Y0m 変数Yの0ヶ月目(ベースライン)の値
Y3m 変数Yの3ヶ月目の値
Y6m 変数Yの6ヶ月目の値

縦持ちデータ

変数 内容 説明
id 個体識別コード
time 測定時点 factor型とする
X 変数 変数Xに対して処理をおこなえば、変数名はX。Yに対して処理すれば、変数名はYとする
Xdiff 変数のベースラインとの差 変数Xに対して処理をおこなえば、変数名はXdiff。Yに対して処理すれば、変数名はYdiffとする。

太字部分のように、処理する変数に対して、変数名も変わるというのが肝である。

準備

とりあえず、いつもの。

library(pacman)
p_load(tidyverse, tidylog)

ダミーデータ作成

下のコードで、先の横持ちデータができます。(seedによってちょっと違うと思います。)

set.seed(1)
id <- 1:10
X0m <- rnorm(length(id), 0, 2)
X3m <- rnorm(length(id), 5, 2)
X6m <- rnorm(length(id), 10, 2)
Y0m <- rnorm(length(id), 100, 10)
Y3m <- rnorm(length(id), 200, 10)
Y6m <- rnorm(length(id), 300, 10)

df00 <- tibble(id, X0m, X3m, X6m, Y0m, Y3m, Y6m)

関数を使わずに、縦持ちデータへ

下のコードで、先の縦持ちデータができます。

temp01 <- df00 %>%
  select(id, starts_with("X")) %>%
  pivot_longer(cols = !id, names_to = "visit", values_to = "X") %>%
  mutate(time =
           case_when(
             str_detect(visit, "0m") == TRUE ~ 0,
             str_detect(visit, "3m") == TRUE ~ 1,
             str_detect(visit, "6m") == TRUE ~ 2
           ),
         time = factor(time, 
                       levels = c(0, 1, 2),
                       labels = c("base", "3m", "6m")
                       )
  ) 

temp02 <- temp01 %>%
  group_by(id) %>%
  filter(time == "base") %>%
  mutate(base = X) %>%
  select(id, base)

temp03 <- left_join(temp01, temp02, by = "id") %>%
  mutate(Xdiff = X - base) %>%
  select(id, time, X, Xdiff)

temp03

これを変数Xだけではなく, 変数Yに対して処理するので関数化したい!

うまくいかない例

long_from_df <- function(var){
  temp01 <- df00 %>%
    select(id, starts_with(var)) %>%
    pivot_longer(cols = !id, names_to = "visit", values_to = var) %>%
    mutate(time =
             case_when(
               str_detect(visit, "0m") == TRUE ~ 0,
               str_detect(visit, "3m") == TRUE ~ 1,
               str_detect(visit, "6m") == TRUE ~ 2
             ),
           time = factor(time, 
                         levels = c(0, 1, 2),
                         labels = c("base", "3m", "6m")
           )
    ) 
  temp02 <- temp01 %>%
    group_by(id) %>%
    filter(time == "base") %>%
    mutate(base = var) %>%
    select(id, base)
  
  temp03 <- left_join(temp01, temp02, by = "id") %>%
    mutate(str_c(var, "diff") = var - base) %>%
    select(id, time, var, str_c(var, "diff"))
  
  temp03
}

long_from_df("X")

データハンドリングではなく、数値計算系の関数だと、こんな感じでうまくいくことは多いが、tidyverse圏だと怒られる。

いざ、うまくいくコードへ

これを解決する方法は、rlangpackageと、それ関連の仕組みです。

うまくいく準備

p_load(rlang)

うまくいくコード

下のコードで

long_from_df <- function(var){

  sym_var <- rlang::sym(var)
  name_var <- rlang::as_name(var)
  
  temp01 <- df00 %>%
    select(id, starts_with(name_var)) %>%
    pivot_longer(cols = !id, names_to = "visit", values_to = name_var) %>%
    mutate(time =
             case_when(
               str_detect(visit, "0m") == TRUE ~ 0,
               str_detect(visit, "3m") == TRUE ~ 1,
               str_detect(visit, "6m") == TRUE ~ 2
             ),
           time = factor(time, 
                         levels = c(0, 1, 2),
                         labels = c("base", "3m", "6m")
           )
    )
  
  temp02 <- temp01 %>%
    group_by(id) %>%
    filter(time == "base") %>%
    mutate(base := !!sym_var) %>%
    select(id, base)
  
  temp03 <- left_join(temp01, temp02, by = "id") %>%
    mutate(!!str_c(name_var, "diff") := !!sym_var - base) %>%
    select(id, time, name_var, !!str_c(name_var, "diff"))
  
  temp03
}

こうすると、

long_from_df("X")

やったー!

うまくいく理由

ぼくは、正直なところ理由が分かりません[1]

キーとなるのは、はじめに作ったrlang::sym(var)rlang::as_name(var)
あと随所にある、:=!!です。

rlang::sym(var)rlang::as_name(var)の中身は、それぞれ変数と文字列になっていて、これのおかげでうまくいくと思います。

:=の使い方は、右辺と左辺のどちらかに!!があると、=:=にしないといけないらしいです。

うまくいくコード(改良版)

@yutannihilation から2点コメントいただきました。
https://twitter.com/yutannihilation/status/1527504675850686464?s=20&t=mzjTrICTjXpv6-DyD6gotg

  • varは元々文字列なので、rlang::as_name(var)は不要
  • mutateの中の!!str_c(var, "diff")は、シンプルに"{var}diff"とかける
long_from_df <- function(var){

  sym_var <- rlang::sym(var)
  
  temp01 <- df00 %>%
    select(id, starts_with(var)) %>%
    pivot_longer(cols = !id, names_to = "visit", values_to = var) %>%
    mutate(time =
             case_when(
               str_detect(visit, "0m") == TRUE ~ 0,
               str_detect(visit, "3m") == TRUE ~ 1,
               str_detect(visit, "6m") == TRUE ~ 2
             ),
           time = factor(time, 
                         levels = c(0, 1, 2),
                         labels = c("base", "3m", "6m")
           )
    )
  
  temp02 <- temp01 %>%
    group_by(id) %>%
    filter(time == "base") %>%
    mutate(base := !!sym_var) %>%
    select(id, base)
  
  temp03 <- left_join(temp01, temp02, by = "id") %>%
    mutate("{var}diff" := !!sym_var - base) %>%
    select(id, time, name_var, !!str_c(var, "diff"))
  
  temp03
}

結論

rlangを使えば、うまくいく。強い方々、理由を教えてください。
関数作りに悩む人のご参考になれば嬉しいです。

参考

参考サイト

https://qiita.com/uri/items/6d5f285280387dff0caa
https://qiita.com/ocean_f/items/d1ceba28cc714936e640
https://thisisnic.github.io/2018/04/16/how-do-i-make-my-own-dplyr-style-functions/
https://rlang.r-lib.org/reference/topic-inject.html#injecting-names-with-

参考ツイート

@KagiyamaNobu のスーパーテクニック
https://twitter.com/KagiyamaNobu/status/1527236207825997825?s=20&t=rqByos8Toa_pV_-UaEPoJg

スーパーバイザー

https://twitter.com/mopcup

脚注
  1. 三角関数を知らなくても自動車乗れると思ってください。 ↩︎

Discussion