Rのtidyverse圏で関数をうまく作る
モチベーション
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圏だと怒られる。
いざ、うまくいくコードへ
これを解決する方法は、rlang
packageと、それ関連の仕組みです。
うまくいく準備
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点コメントいただきました。
-
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
を使えば、うまくいく。強い方々、理由を教えてください。
関数作りに悩む人のご参考になれば嬉しいです。
参考
参考サイト
参考ツイート
@KagiyamaNobu のスーパーテクニック
スーパーバイザー
-
三角関数を知らなくても自動車乗れると思ってください。 ↩︎
Discussion