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 31 88 -72 394
1 b 43 -62 358 398
1 c -157 180 403 160
2 a 185 179 79 225
2 b 96 230 291 214
2 c 10 126 508 336

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

id pre_post a_x a_y b_x b_y c_x c_y
1 post 88 394 -62 398 180 160
1 pre 31 -72 43 358 -157 403
2 post 179 225 230 214 126 336
2 pre 185 79 96 291 10 508
3 post 105 265 52 369 101 459
3 pre 6 -435 40 272 124 -457

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

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 88 394 -62 398 180 160
1 pre 31 -72 43 358 -157 403
2 post 179 225 230 214 126 336
2 pre 185 79 96 291 10 508
3 post 105 265 52 369 101 459
3 pre 6 -435 40 272 124 -457

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 31
1 b x_pre 43
1 c x_pre -157
2 a x_pre 185
2 b x_pre 96
2 c x_pre 10

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

res <- res %>% 
  separate(var_name, c("var", "pre_post"))
knitr::kable(head(res))
id group var pre_post value
1 a x pre 31
1 b x pre 43
1 c x pre -157
2 a x pre 185
2 b x pre 96
2 c x pre 10

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

res <- res %>% 
  unite(new_var, group, var)
knitr::kable(head(res))
id new_var pre_post value
1 a_x pre 31
1 b_x pre 43
1 c_x pre -157
2 a_x pre 185
2 b_x pre 96
2 c_x pre 10

あとはこの変数名の列を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 88 394 -62 398 180 160
1 pre 31 -72 43 358 -157 403
2 post 179 225 230 214 126 336
2 pre 185 79 96 291 10 508
3 post 105 265 52 369 101 459
3 pre 6 -435 40 272 124 -457

これで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 102 -109 kosaki kosaki
1 b 187 35 kosaki kosaki
1 c -1 173 chitoge kosaki
2 a 0 109 kosaki kosaki
2 b 38 137 kosaki kosaki
2 c -78 125 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 -109 kosaki 35 kosaki 173 kosaki
1 pre 102 kosaki 187 kosaki -1 chitoge
2 post 109 kosaki 137 kosaki 125 kosaki
2 pre 0 kosaki 38 kosaki -78 chitoge
3 post 271 kosaki 164 kosaki 357 kosaki
3 pre -151 kosaki 129 chitoge -152 kosaki

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