1 Case 1: multi-gather/spread問題

1.1 Question 1

以下のようなデータがあります:

# 資料を読んでる人が再現できるように、データ生成用コードも残しときます
n = 30
df_1 <- data.frame(
  id = rep(1:10, each = 3),
  group = rep(letters[1:3], 10),
  x_pre = round(rnorm(n) * 100),
  x_post = round(rnorm(n, 1, 1) * 100),
  y_pre = round(rnorm(n, 2, 3) * 100),
  y_post = round(rnorm(n, 3, 1) * 100)
)
knitr::kable(head(df_1))
id group x_pre x_post y_pre y_post
1 a -12 139 405 400
1 b -42 -147 -101 301
1 c 118 -34 409 376
2 a -42 150 807 362
2 b -10 168 251 282
2 c -139 66 291 400

これを、以下のようにしたいです:

id pre_post a_x a_y b_x b_y c_x c_y
1 post 139 400 -147 301 -34 376
1 pre -12 405 -42 -101 118 409
2 post 150 362 168 282 66 400
2 pre -42 807 -10 251 -139 291
3 post 75 290 105 431 76 438
3 pre 140 -204 18 164 24 -95

どうしたらいいのでしょうか?

1.2 Answer

以下のようにやります:

library(tidyverse)
df_1_result <-df_1 %>%
  gather(key = var_name, value = value, -c(id, group)) %>%
  separate(var_name, c("var", "pre_post")) %>%
  unite(new_var, group, var) %>%
  spread(key = new_var, value = value)
knitr::kable(head(df_1_result))
id pre_post a_x a_y b_x b_y c_x c_y
1 post 139 400 -147 301 -34 376
1 pre -12 405 -42 -101 118 409
2 post 150 362 168 282 66 400
2 pre -42 807 -10 251 -139 291
3 post 75 290 105 431 76 438
3 pre 140 -204 18 164 24 -95

1.3 解説

1.3.1 考え方

通称「multi-gather, multi-spread問題」の一種です。

wide-long変換をするにはtidyr::gathertidyr::spreadを使えばいいのですが、今回は単純にそれらを使うだけではうまく行きません。そこで以下のようなアプローチをします:

  1. 一旦tidyなデータ(long data)に整形
  2. 変数名を切り離す
  3. 目的の変数名を作成
  4. 新たに作った変数名をkeyにしてwideに展開

1.3.2 手順

まずはgather:

res <- df_1 %>%
  gather(key = var_name, value = value, -c(id, group))
knitr::kable(head(res))
id group var_name value
1 a x_pre -12
1 b x_pre -42
1 c x_pre 118
2 a x_pre -42
2 b x_pre -10
2 c x_pre -139

ここからがポイントで、当初の変数名を2つに切り離します:

res <- res %>%
  separate(var_name, c("var", "pre_post"))
knitr::kable(head(res))
id group var pre_post value
1 a x pre -12
1 b x pre -42
1 c x pre 118
2 a x pre -42
2 b x pre -10
2 c x pre -139

これで要素がちゃんと分かれたデータになりました。そして目的の変数名になるようひっつけます:

res <- res %>%
  unite(new_var, group, var)
knitr::kable(head(res))
id new_var pre_post value
1 a_x pre -12
1 b_x pre -42
1 c_x pre 118
2 a_x pre -42
2 b_x pre -10
2 c_x pre -139

あとはこの変数名の列をkeyとしてwideにします:

res <- res %>%
  spread(key = new_var, value = value)
knitr::kable(head(res))
id pre_post a_x a_y b_x b_y c_x c_y
1 post 139 400 -147 301 -34 376
1 pre -12 405 -42 -101 118 409
2 post 150 362 168 282 66 400
2 pre -42 807 -10 251 -139 291
3 post 75 290 105 431 76 438
3 pre 140 -204 18 164 24 -95

これでOKです。

1.3.3 応用

今回はvalueにあたるデータが全て数値だったのでスムーズでしたが、型が違う場合もあります:

n = 30
df_1a <- data.frame(
  id = rep(1:10, each = 3),
  group = rep(letters[1:3], 10),
  x_pre = round(rnorm(n) * 100),
  x_post = round(rnorm(n, 1, 1) * 100),
  y_pre = sample(c("kosaki", "chitoge"), n, replace = TRUE, prob = c(5, 5)),
  y_post = sample(c("kosaki", "chitoge"), n, replace = TRUE, prob = c(9, 1))
)
knitr::kable(head(df_1a))
id group x_pre x_post y_pre y_post
1 a 9 -42 kosaki kosaki
1 b -23 198 kosaki kosaki
1 c -5 52 kosaki kosaki
2 a -39 4 chitoge chitoge
2 b 96 198 kosaki kosaki
2 c -8 20 chitoge kosaki

この場合、まずは気にせずに同じように整形し、あとから列の型を変更すればOKです

res_a <- df_1a %>%
  # このときvalueがcharacter型になる
  gather(key = var_name, value = value, -c(id, group)) %>%
  # 気にせず処理
  separate(var_name, c("var", "pre_post")) %>%
  unite(new_var, group, var) %>%
  spread(new_var, value) %>%
  # 数値にしたい列を変換
  mutate_at(vars(ends_with("_x")), as.numeric)

knitr::kable(head(res_a))
id pre_post a_x a_y b_x b_y c_x c_y
1 post -42 kosaki 198 kosaki 52 kosaki
1 pre 9 kosaki -23 kosaki -5 kosaki
2 post 4 chitoge 198 kosaki 20 kosaki
2 pre -39 chitoge 96 kosaki -8 chitoge
3 post 71 kosaki -133 kosaki 60 kosaki
3 pre -49 kosaki -49 kosaki -49 chitoge

以上です。なお個人的には全てtidyにしたいです。