使用 tidyverse 整理 table 多组宽列

Tidying table with multiple groups of wide columns, using tidyverse

我经常发现自己有一个包含多组宽列的 table,如下所示:

  replicate groupA        VA1         VA2 groupB         VB1        VB2
1         1      a  0.3429166 -2.30336406      f  0.05363582  1.6454078
2         2      b -1.3183732 -0.13516849      g -0.42586417  0.1541541
3         3      c -0.7908358 -0.10746447      h  1.05134242  1.4297350
4         4      d -0.9963677 -1.82557058      i -1.14532536  1.0815733
5         5      e -1.3634609  0.04385812      j -0.65643595 -0.1452877

我想将这些列变成一列 table,像这样:

   replicate group key       value
1          1     a  V1  0.34291665
2          2     b  V1 -1.31837322
3          3     c  V1 -0.79083580
4          4     d  V1 -0.99636772
5          5     e  V1 -1.36346088
6          1     a  V2 -2.30336406
7          2     b  V2 -0.13516849
8          3     c  V2 -0.10746447
9          4     d  V2 -1.82557058
10         5     e  V2  0.04385812
11         1     f  V1  0.05363582
12         2     g  V1 -0.42586417
13         3     h  V1  1.05134242
14         4     i  V1 -1.14532536
15         5     j  V1 -0.65643595
16         1     f  V2  1.64540784
17         2     g  V2  0.15415408
18         3     h  V2  1.42973499
19         4     i  V2  1.08157329
20         5     j  V2 -0.14528774

我可以通过单独选择两组列,整理,然后将它们绑定在一起(下面的代码)来做到这一点。但是,这种方式看起来不是特别优雅,如果超过两组列,就会变得很麻烦。我想知道是否有更优雅的方法,使用单个数据转换管道链。

这里的基本问题是:我们如何自动化将 table 分成几组列、整理这些列,然后重新组合在一起的过程。

我当前的代码:

library(dplyr)
library(tidyr)

# generate example code
df_wide <- data.frame(replicate = 1:5,
                      groupA = letters[1:5],
                      VA1 = rnorm(5),
                      VA2 = rnorm(5),
                      groupB = letters[6:10],
                      VB1 = rnorm(5),
                      VB2 = rnorm(5))

# tidy columns with A in the name
dfA <- select(df_wide, replicate, groupA, VA1, VA2) %>%
  gather(key, value, VA1, VA2) %>%
  mutate(key = case_when(key == "VA1" ~ "V1",
                         key == "VA2" ~ "V2")) %>%
  select(replicate, group = groupA, key, value)

# tidy columns with B in the name
dfB <- select(df_wide, replicate, groupB, VB1, VB2) %>%
  gather(key, value, VB1, VB2) %>%
  mutate(key = case_when(key == "VB1" ~ "V1",
                         key == "VB2" ~ "V2")) %>%
  select(replicate, group = groupB, key, value)

# combine
df_long <- rbind(dfA, dfB)

注意:有人问过类似的问题here and here,但我认为接受的答案表明这是一个略有不同的问题。

1

虽然问题要求 tidyverse 解决方案,但有一个方便的选项 melt 来自 data.table,它也可以在 measure 参数。

library(data.table)
setnames(melt(melt(setDT(df1), measure = patterns('group', 'VA', 'VB')), 
        id.var = 1:3)[, -4, with = FALSE], 2:3, c('key', 'group'))[]

2。一个

tidyverse 我们可以将数据集子集化为 list,然后循环遍历 listmap_df 将其转换为 'long' 格式 gather 得到一个 data.frame

library(tidyverse)
list(df1[1:4], df1[c(1,5:7)]) %>%
      map_df(~gather(., key, value, 3:4) %>%
                   {names(.)[2] <- 'group';.}) %>%
      mutate(key = sub('(.).(.)', '\1\2', key))
#   replicate group key       value
#1          1     a  V1  0.34291660
#2          2     b  V1 -1.31837320
#3          3     c  V1 -0.79083580
#4          4     d  V1 -0.99636770
#5          5     e  V1 -1.36346090
#6          1     a  V2 -2.30336406
#7          2     b  V2 -0.13516849
#8          3     c  V2 -0.10746447
#9          4     d  V2 -1.82557058
#10         5     e  V2  0.04385812
#11         1     f  V1  0.05363582
#12         2     g  V1 -0.42586417
#13         3     h  V1  1.05134242
#14         4     i  V1 -1.14532536
#15         5     j  V1 -0.65643595
#16         1     f  V2  1.64540780
#17         2     g  V2  0.15415410
#18         3     h  V2  1.42973500
#19         4     i  V2  1.08157330
#20         5     j  V2 -0.14528770

2.b

如果我们需要split基于'group'

的出现
split.default(df1[-1], cumsum(grepl('group', names(df1)[-1]))) %>% 
         map(~bind_cols(df1[1], .)) %>% 
         map_df(~gather(., key, value, 3:4) %>% 
               {names(.)[2] <- 'group';.}) %>%
         mutate(key = sub('(.).(.)', '\1\2', key))

2.c

本着 tidyverse 选项

的精神,包括 rename_at 而不是 names 赋值
df1[-1] %>% 
      split.default(cumsum(grepl('group', names(df1)[-1]))) %>% 
      map_df(~bind_cols(df1[1], .) %>% 
           gather(., key, value, 3:4) %>%
           rename_at(2, funs(substring(.,1, 5))))

注意:

1) 2.a2.b2.c 都使用了 tidyverse 函数

2) 不依赖于列名

中的子字符串'A'或'B'

3) 假设 OP 数据集中的模式将是 'group' 后跟值列

1) 这个解决方案包括:

  • gather 生成所需的行数
  • a mutate 组合了 groupA 和 groupB 列,并将键列更改为请求的列和
  • select 挑选出想要的列。

首先收集名称以 V 开头的列,然后从 groupA 和 groupB 创建一个新的组列,如果键中有 A,则选择 groupA,如果键中有 B,则选择 groupB。 (我们在这里使用 mapply(switch, ...) 来轻松扩展到 3+ 组的情况,但我们可以使用 ifelse,即 ifelse(grepl("A", key), as.character( groupA), as.character(groupB)), 假设我们只有两个组。) mutate 还将键名从 VA1 减少到 V1 等,最后 select 出所需的列。

DF %>% 
   gather(key, value, starts_with("V")) %>%
   mutate(group = mapply(switch, gsub("[^AB]", "", key), A = groupA, B = groupB),
          key = sub("[AB]", "", key)) %>%
   select(replicate, group, key, value)

给予:

   replicate group key       value
1          1     a  V1  0.34291660
2          2     b  V1 -1.31837320
3          3     c  V1 -0.79083580
4          4     d  V1 -0.99636770
5          5     e  V1 -1.36346090
6          1     a  V2 -2.30336406
7          2     b  V2 -0.13516849
8          3     c  V2 -0.10746447
9          4     d  V2 -1.82557058
10         5     e  V2  0.04385812
11         1     f  V1  0.05363582
12         2     g  V1 -0.42586417
13         3     h  V1  1.05134242
14         4     i  V1 -1.14532536
15         5     j  V1 -0.65643595
16         1     f  V2  1.64540780
17         2     g  V2  0.15415410
18         3     h  V2  1.42973500
19         4     i  V2  1.08157330
20         5     j  V2 -0.14528770

2) 另一种方法是将列拆分为组,这样在从名称中删除 A 和 B 后,组中的所有列都具有相同的名称。对每个这样的组执行 unlist 以将列表缩减为普通向量列表并将该列表转换为 data.frame。最后收集 V 列并重新排列。请注意,rownames_to_column 来自 tibble 包。

DF %>%
   as.list %>%
   split(sub("[AB]", "", names(.))) %>%
   lapply(unlist) %>%
   as.data.frame %>%
   rownames_to_column %>%
   gather(key, value, starts_with("V")) %>%
   arrange(gsub("[^AB]", "", rowname), key) %>%
   select(replicate, group, key, value)

2a) 如果行顺序不重要,则可以省略 rownames_to_column、排列和 select 行,将其缩短为:

DF %>%
   as.list %>%
   split(sub("[AB]", "", names(.))) %>%
   lapply(unlist) %>%
   as.data.frame %>%
   gather(key, value, starts_with("V"))

解决方案 (2) 和 (2a) 可以很容易地转换为仅基于基础的解决方案,方法是将 gather 替换为来自基础的适当整形,如在第二次整形中,即在 (3) 中产生 d2 的整形。

3) 尽管问题要求 tidyverse 解决方案,但有一个相当方便的基本解决方案,由两个重塑调用组成。拆分产生的变化是:list(group = c("groupA", "groupB"), V1 = c("VA1", "VB1"), V2 = c("VA2", "VB2"))——它与每组列中的第 i 列匹配。

varying <- split(names(DF)[-1], gsub("[AB]", "", names(DF))[-1])
d <- reshape(DF, dir = "long", varying = varying, v.names = names(varying))
d <- subset(d, select = -c(time, id))

d2 <- reshape(d, dir = "long", varying = list(grep("V", names(d))), v.names = "value", 
  timevar = "key")
d2 <- subset(d2, select = c(replication, group, key, value))

d2

注意:可重现形式的输入是:

DF <- structure(list(replicate = 1:5, groupA = structure(1:5, .Label = c("a", 
"b", "c", "d", "e"), class = "factor"), VA1 = c(0.3429166, -1.3183732, 
-0.7908358, -0.9963677, -1.3634609), VA2 = c(-2.30336406, -0.13516849, 
-0.10746447, -1.82557058, 0.04385812), groupB = structure(1:5, .Label = c("f", 
"g", "h", "i", "j"), class = "factor"), VB1 = c(0.05363582, -0.42586417, 
1.05134242, -1.14532536, -0.65643595), VB2 = c(1.6454078, 0.1541541, 
1.429735, 1.0815733, -0.1452877)), .Names = c("replicate", "groupA", 
"VA1", "VA2", "groupB", "VB1", "VB2"), class = "data.frame", row.names = c("1", 
"2", "3", "4", "5"))