按照先前重塑的索引列重塑几组变量

时间:2020-10-10 06:39:45

标签: r data.table reshape tidyr

我正在尝试将R中的data.table从宽变长为整形。我需要减少几组变量,但是我一次做一组的最初方法看起来很容易出错,我想要替代。在这个可重现的示例中,我以与原始数据相似的方式创建了两组变量(XXYYY)。

我的解决方案在此示例中有效,但是原始数据表具有如此多的列,以至于我不信任此代码。我不确定问题出在我的实现中还是方法本身-如果可能的话,我希望使用keep it simple

问:是否有更好的方法来解决此问题?

示例数据

library(data.table)

dt.orig <- data.table(ID= 1:3,
                      a = c("Y", "Y", "N"),
                      b = c("N", "Y", "Y"),
                      XXa=c(101, 102, 103),
                      XXb=c(110, 120, 130),
                      YYYa=c(201, 202, 203),
                      YYYb=c(210, 220, 230))


dt.goal <- data.table(ID=c(1,1,2,2,3,3),
                      obs=c("a", "b"),
                      outcome = c("Y", "N", "Y", "Y", "N", "Y"),
                      XX=c(101, 110, 102, 120, 103, 130),
                      YYY=c(201, 210, 202, 220, 203, 230))

> dt.orig
   ID a b XXa XXb YYYa YYYb
1:  1 Y N 101 110  201  210
2:  2 Y Y 102 120  202  220
3:  3 N Y 103 130  203  230
> dt.goal
   ID obs outcome  XX YYY
1:  1   a       Y 101 201
2:  1   b       N 110 210
3:  2   a       Y 102 202
4:  2   b       Y 120 220
5:  3   a       N 103 203
6:  3   b       Y 130 230

dt.orig代表原始数据,dt.goal是我要实现的目标。在tidyr封装插图之后,我的初步尝试如下:

尝试1:tidyr/dplyr方法

library(tidyr)
library(dplyr)

dt.orig[, .(ID, a, b)] %>%
  pivot_longer(
    cols = c("a", "b"),
    names_to = "obs",
    values_to = "outcome"
  ) %>% data.table -> dt.tidyr1

dt.orig[, .(ID, XXa, XXb, YYYa, YYYb)] %>%
  pivot_longer(
             cols = XXa:YYYb,
             names_to = c(".value", "obs"),
             names_pattern = "(XX|YYY)(.)",
              ) %>% data.table -> dt.tidyr2

dt.tidyr1[, .(ID, obs, outcome)] == dt.goal[, .(ID, obs, outcome)] # test passes
dt.tidyr2[, .(ID, obs, XX, YYY)] == dt.goal[, .(ID, obs, XX, YYY)] # test passes

> merge(dt.tidyr1, dt.tidyr2)
   ID obs outcome  XX YYY
1:  1   a       Y 101 201
2:  1   b       N 110 210
3:  2   a       Y 102 202
4:  2   b       Y 120 220
5:  3   a       N 103 203
6:  3   b       Y 130 230

在上面的代码中,我首先为obsab的结果创建一对名称/值。由于所有变量组在其命名方案中都包含ab,因此我可以使用这一事实通过单个 regex 遍历所有组。

然后我可以将两个数据表合并或合并为最终数据表。

尝试2:data.table方式

按照相同的原理,我可以将原始的a和b融化为obs和结果,然后为每个var组进行第二步(为简洁起见,此处未显示)。在这种情况下,我一次成功融化了一个var组,因此在此示例中,首先进行所有XX,然后进行所有YYY。优点/缺点:优点是我不需要创建多个步骤表即可完成该过程。缺点:世界上没有足够的咖啡来完成这种方法,而实际数据中的所有var组都没有这种咖啡(并相信结果)。

dt.melt1 <- melt(dt.orig,
                 id.vars = c("ID", "XXa", "XXb", "YYYa", "YYYb"),
                 measure = c("a", "b"),
                 variable.name = "obs",
                 value.name = "outcome")

3 个答案:

答案 0 :(得分:1)

我认为dplyr版本很好。您可以只使pivot_longer中的正则表达式更通用,以扩大列数。另外,您可以将a和b列展平到列表中,这样就不必处理第二个数据框并合并。

# flatten cols a,b 
outcome <-  c(t(select(dt.orig, c(a, b))))

# pivot longer on regex and add outcome list
dt.orig %>%  
  pivot_longer(-c(ID, a, b), 
               names_to = c(".value", "obs"),
               names_pattern = "(.*)(.)") %>% 
  mutate(outcome = outcome) %>% 
  select(-c(a, b))



     ID obs      XX   YYY outcome
1     1 a       101   201 Y      
2     1 b       110   210 N      
3     2 a       102   202 Y      
4     2 b       120   220 Y      
5     3 a       103   203 N      
6     3 b       130   230 Y      

答案 1 :(得分:1)

我不认为data.table::melt具有将您的“ XXa”自动拆分为“ XX”和“ a”的机制,因此您可能别无选择,只能通过{ {1}}。但是,这是获得结果的两种替代方法,它们是针对@LRRR的漂亮tidyverse解决方案的快速基准测试。

数据和库:

data.table

第一个library(data.table) library(tidyverse) library(microbenchmark) dt.orig = data.table(ID= 1:3, a = c("Y", "Y", "N"), b = c("N", "Y", "Y"), XXa=c(101, 102, 103), XXb=c(110, 120, 130), YYYa=c(201, 202, 203), YYYb=c(210, 220, 230)) 解决方案(用于基准测试的函数包装):

data.table

第二个dt_1 <- function() { dt = melt(dt.orig, id.vars=c("a", "b", "ID"), measure.vars=patterns("XX|YYY"), variable.factor=FALSE) dt = melt(dt, id.vars=c("ID", "variable", "value"), value.name="outcome", variable.name="obs", variable.factor=FALSE) dt = dt[substr(variable, nchar(variable), nchar(variable)) == obs] dt[, variable := substr(variable, 1, nchar(variable)-1)] dcast(dt, ID + obs + outcome ~ variable) } 解决方案:

data.table

dt_2 <- function() { # ID-obs-outcome dt1 = melt(dt.orig[, .(ID, a, b)], id.vars="ID", value.name="outcome", variable.name="obs", variable.factor=FALSE) # ID-obs-XX-YYY dt2 = melt(dt.orig[, !c("a", "b")], id.vars="ID", variable.factor=FALSE) dt2[, obs := substr(variable, nchar(variable), nchar(variable))] dt2[, variable := substr(variable, 1, nchar(variable)-1)] dt2 = dcast(dt2, ID + obs ~ variable) # merge merge(dt1, dt2, by=c("ID", "obs")) } 解决方案被LRRR发布为工作答案:

tidyverse

基准:

tidy_1 <- function(){
  # flatten cols a,b 
  outcome <-  c(t(select(dt.orig, c(a, b))))
  # pivot longer on regex and add outcome list
  dt.orig %>%  
    pivot_longer(-c(ID, a, b), 
                 names_to = c(".value", "obs"),
                 names_pattern = "(.*)(.)") %>% 
    mutate(outcome = outcome) %>% 
    select(-c(a, b))
}

答案 2 :(得分:1)

您可以通过两行来实现:

dt.res <- melt(dt.orig, "ID", patterns("^[ab]$", "^XX", "^YYY"), "obs", c("outcome", "XX", "YYY"))
setorder(dt.res, ID)[, obs := unname(c("1" = "a", "2" = "b")[obs])]

输出

> dt.res[]
   ID obs outcome  XX YYY
1:  1   a       Y 101 201
2:  1   b       N 110 210
3:  2   a       Y 102 202
4:  2   b       Y 120 220
5:  3   a       N 103 203
6:  3   b       Y 130 230

这是上面相同代码的稍长版本

dt.res <- 
  melt(
    dt.new2, 
    id.vars = "ID", measure.vars = patterns("^[ab]$", "^XX", "^YYY"), 
    variable.name = "obs", value.name = c("outcome", "XX", "YYY")
  )
setorder(dt.res, ID)[, obs := unname(c("1" = "a", "2" = "b")[obs])]

其他说明

似乎除了ID列外,您还有三组列需要同时进行透视:没有前缀的列(即ab),前缀XX和带有前缀YYY的前缀。如果每个组中的后缀为a的列始终出现在后缀为b的列之前,则可以同时melt将这些列组作为{{ 1}},并在本机之后支持此类操作。您需要使用data.table v1.9.6指定每个列组。

这就是我们拥有regex的原因,它捕获了我们尝试patterns("^[ab]$", "^XX", "^YYY")的三个列组。经过melt操作后,您将获得一个melt,如下所示:

data.table

我们在 ID obs outcome XX YYY 1: 1 1 Y 101 201 2: 2 1 Y 102 202 3: 3 1 N 103 203 4: 1 2 N 110 210 5: 2 2 Y 120 220 6: 3 2 Y 130 230 中得到了ab,而不是12,因为obs操作会自动在其中设置第一个匹配项每个组分别为melt,第二个组为"1",依此类推。稍后,我们可以通过指定"2""1" = "a"重置此列。但是,您可能已经知道,如果后缀为"2" = "b"的列出现在后缀为a的列之后,那么我们将无法再使用此映射b。这就是为什么我们必须确保每个列组的顺序正确。

有关此订购问题的更好说明,请参见下面的代码:

c("1" = "a", "2" = "b")

因此,如果您不能确保每个组中的顺序,则可以进行预处理以固定列顺序。这样,您也可以获得正确的结果。

# Assume that your data.table looks like this
> dt.unordered
   ID b a XXa YYYb XXb YYYa
1:  1 N Y 101  210 110  201
2:  2 Y Y 102  220 120  202
3:  3 Y N 103  230 130  203

# See the difference now?
> dt.wrong <- melt(dt.unordered, "ID", patterns("^[ab]$", "^XX", "^YYY"), "obs", c("outcome", "XX", "YYY"))
> dt.wrong[]
   ID obs outcome  XX YYY
1:  1   1       N 101 210
2:  2   1       Y 102 220
3:  3   1       Y 103 230
4:  1   2       Y 110 201
5:  2   2       Y 120 202
6:  3   2       N 130 203

总而言之,如果您已对所有列进行了预排序,请执行以下操作:

> setcolorder(dt.unordered, sort(names(dt.unordered)))
> dt.fixed <- melt(dt.unordered, "ID", patterns("^[ab]$", "^XX", "^YYY"), "obs", c("outcome", "XX", "YYY"))
> dt.fixed[]
   ID obs outcome  XX YYY
1:  1   1       Y 101 201
2:  2   1       Y 102 202
3:  3   1       N 103 203
4:  1   2       N 110 210
5:  2   2       Y 120 220
6:  3   2       Y 130 230

如果没有,请执行以下操作:

dt.res <- melt(dt.orig, "ID", patterns("^[ab]$", "^XX", "^YYY"), "obs", c("outcome", "XX", "YYY"))
setorder(dt.res, ID)[, obs := unname(c("1" = "a", "2" = "b")[obs])]