【问题标题】:Reshaping multiple sets of measurement columns (wide format) into single columns (long format)将多组测量列(宽格式)重塑为单列(长格式)
【发布时间】:2012-09-10 02:18:42
【问题描述】:

我有一个宽格式的数据框,在不同的日期范围内进行了重复测量。在我的示例中,有三个不同的时期,它们都有对应的值。例如。第一次测量(Value1)是在DateRange1StartDateRange1End 期间测量的:

ID DateRange1Start DateRange1End Value1 DateRange2Start DateRange2End Value2 DateRange3Start DateRange3End Value3
1 1/1/90 3/1/90 4.4 4/5/91 6/7/91 6.2 5/5/95 6/6/96 3.3 

我希望将数据重塑为长格式,以便将 DateRangeXStart 和 DateRangeXEnd 列分组。因此,原表中的 1 行变为新表中的 3 行:

ID DateRangeStart DateRangeEnd Value
1 1/1/90 3/1/90 4.4
1 4/5/91 6/7/91 6.2
1 5/5/95 6/6/96 3.3

我知道必须有一种方法可以使用reshape2/melt/recast/tidyr,但我似乎无法弄清楚如何将多组度量变量映射到以这种特殊方式单组值列。

【问题讨论】:

  • 作为一般做法,您可能希望将来有一个更好的命名模式。例如,使用“DateRangeStart1”、“DateRangeEnd1”、“Value1”(换句话说,“VariableMeasurement”)比将测量值卡在变量名称中的某处要容易/干净得多。
  • 答案必须使用reshape2/melt/recast/tidyr吗? (如果不是,这个问题会成为一个更好、更普遍的欺骗目标)

标签: r reshape tidyr reshape2 r-faq


【解决方案1】:
reshape(dat, idvar="ID", direction="long", 
             varying=list(Start=c(2,5,8), End=c(3,6,9), Value=c(4,7,10)),
             v.names = c("DateRangeStart", "DateRangeEnd", "Value") )
#-------------
    ID time DateRangeStart DateRangeEnd Value
1.1  1    1          1/1/90        3/1/90    4.4
1.2  1    2          4/5/91        6/7/91    6.2
1.3  1    3          5/5/95        6/6/96    3.3

(根据 Josh 的建议添加了 v.names。)

【讨论】:

  • +1 用于炫耀 varying= 论点的力量。接下来,v.names 参数也可以修饰这些列名,如下所示:v.names = c("DateRangeStart", "DateRangeEnd", "Value")
【解决方案2】:

data.tablemelt 函数可以融合成多个列。使用它,我们可以简单地做到:

require(data.table)
melt(setDT(dat), id=1L,
     measure=patterns("Start$", "End$", "^Value"), 
     value.name=c("DateRangeStart", "DateRangeEnd", "Value"))

#    ID variable DateRangeStart DateRangeEnd Value
# 1:  1        1         1/1/90       3/1/90   4.4
# 2:  1        2         4/5/91       6/7/91   6.2
# 3:  1        3         5/5/95       6/6/96   3.3

或者,您也可以通过列位置引用三组度量列:

melt(setDT(dat), id = 1L, 
     measure = list(c(2,5,8), c(3,6,9), c(4,7,10)), 
     value.name = c("DateRangeStart", "DateRangeEnd", "Value"))

【讨论】:

    【解决方案3】:

    1.0.0 版本开始,使用 tidyr 包的函数 pivot_longer() 可以将具有多个值/度量列的宽格式重新整形为长格式。

    这优于gather() 之前的 tidyr 策略而不是 spread()(请参阅@AndrewMacDonald 的回答),因为不再删除属性(在下面的示例中,日期仍然是日期,数字仍然是数字)。

    library("tidyr")
    library("magrittr")
    
    a <- structure(list(ID = 1L, 
                        DateRange1Start = structure(7305, class = "Date"), 
                        DateRange1End = structure(7307, class = "Date"), 
                        Value1 = 4.4, 
                        DateRange2Start = structure(7793, class = "Date"),
                        DateRange2End = structure(7856, class = "Date"), 
                        Value2 = 6.2, 
                        DateRange3Start = structure(9255, class = "Date"), 
                        DateRange3End = structure(9653, class = "Date"), 
                        Value3 = 3.3),
                   row.names = c(NA, -1L), class = c("tbl_df", "tbl", "data.frame"))
    

    pivot_longer()(对应方:pivot_wider())的工作方式类似于gather()。 但是,它提供了额外的功能,例如多值列。 只有一个值列,宽数据集的所有列名将进入一个长列,名称在names_to 中给出。 对于多个值列,names_to 可能会收到多个新名称。

    如果所有列名都遵循特定的模式,例如 Start_1End_1Start_2 等,这是最简单的。 因此,我在第一步重命名了列。

    (names(a) <- sub("(\\d)(\\w*)", "\\2_\\1", names(a)))
    #>  [1] "ID"               "DateRangeStart_1" "DateRangeEnd_1"  
    #>  [4] "Value_1"          "DateRangeStart_2" "DateRangeEnd_2"  
    #>  [7] "Value_2"          "DateRangeStart_3" "DateRangeEnd_3"  
    #> [10] "Value_3"
    
    pivot_longer(a, 
                 cols = -ID, 
                 names_to = c(".value", "group"),
                 # names_prefix = "DateRange",
                 names_sep = "_")
    #> # A tibble: 3 x 5
    #>      ID group DateRangeEnd DateRangeStart Value
    #>   <int> <chr> <date>       <date>         <dbl>
    #> 1     1 1     1990-01-03   1990-01-01       4.4
    #> 2     1 2     1991-07-06   1991-05-04       6.2
    #> 3     1 3     1996-06-06   1995-05-05       3.3
    

    或者,可以使用 pivot 规范 完成重塑,该规范提供更精细的控制(请参阅下面的链接):

    spec <- a %>%
        build_longer_spec(cols = -ID) %>%
        dplyr::transmute(.name = .name,
                         group = readr::parse_number(name),
                         .value = stringr::str_extract(name, "Start|End|Value"))
    
    pivot_longer(a, spec = spec)
    

    reprex package (v0.2.1) 于 2019 年 3 月 26 日创建

    另见:https://tidyr.tidyverse.org/articles/pivot.html

    【讨论】:

    • 这实际上是对一个稍微不同的问题的回答,即如何使用 tidy-methods 避免属性丢失。最初接受的答案(使用stats::reshape)从来没有这个问题。原始问题显然也没有日期分类变量。 reshape 函数保留了因子水平和日期类。
    • 我完全同意您的 stats::reshape() 解决方案 (+1) 也同样出色。
    • 正则表达式可以简化为names(a) &lt;- sub("(\\d)(\\w*)", "\\2_\\1", names(a))
    【解决方案4】:

    这是使用tidyr 解决问题的方法。这是它的函数extract_numeric() 的一个有趣用例,我用来从列名中提取组

    library(dplyr)
    library(tidyr)
    
    a <- read.table(textConnection("
    ID DateRange1Start DateRange1End Value1 DateRange2Start DateRange2End Value2 DateRange3Start DateRange3End Value3
    1 1/1/90 3/1/90 4.4 4/5/91 6/7/91 6.2 5/5/95 6/6/96 3.3 
    "),header=TRUE)
    
    a %>%
      gather(variable,value,-ID) %>%
      mutate(group = extract_numeric(variable)) %>%
      mutate(variable =  gsub("\\d","",x = variable)) %>%
      spread(variable,value)
    
      ID group DateRangeEnd DateRangeStart Value
    1  1     1       3/1/90         1/1/90   4.4
    2  1     2       6/7/91         4/5/91   6.2
    3  1     3       6/6/96         5/5/95   3.3
    

    【讨论】:

      【解决方案5】:

      两个附加选项(示例数据框超过一行以更好地显示代码的工作):

      1) 以 R 为基数:

      l <- lapply(split.default(d[-1], cumsum(grepl('Start$', names(d)[-1]))),
                  setNames, c('DateRangeStart','DateRangeEnd','Value'))
      data.frame(ID = d[,1], do.call(rbind, l), row.names = NULL)
      

      给出:

        ID DateRangeStart DateRangeEnd Value
      1  1         1/1/90       3/1/90   4.4
      2  2         1/2/90       3/2/90   6.1
      3  1         4/5/91       6/7/91   6.2
      4  2         4/6/91       6/8/91   3.2
      5  1         5/5/95       6/6/96   3.3
      6  2         5/5/97       6/6/98   1.3
      

      2) 使用tidyverse

      library(dplyr)
      library(purrr)
      
      split.default(d[-1], cumsum(grepl('Start$', names(d)[-1]))) %>%
        map_dfr(~set_names(., c('DateRangeStart','DateRangeEnd','Value'))) %>% 
        bind_cols(ID = rep(d$ID, nrow(.)/nrow(d)), .)
      

      3) 使用sjmisc-package:

      library(sjmisc)
      to_long(d, keys = 'group',
              values = c('DateRangeStart','DateRangeEnd','Value'), 
              c('DateRange1Start','DateRange2Start','DateRange3Start'),
              c('DateRange1End','DateRange2End','DateRange3End'),
              c('Value1','Value2','Value3'))[,-2]
      

      如果您还想要一个组/时间列,您可以将上述方法调整为:

      1) 以 R 为基数:

      l <- lapply(split.default(d[-1], cumsum(grepl('Start$', names(d)[-1]))),
                  setNames, c('DateRangeStart','DateRangeEnd','Value'))
      data.frame(ID = d[,1],
                 group = rep(seq_along(l), each = nrow(d)),
                 do.call(rbind, l), row.names = NULL)
      

      给出:

        ID group DateRangeStart DateRangeEnd Value
      1  1     1         1/1/90       3/1/90   4.4
      2  2     1         1/2/90       3/2/90   6.1
      3  1     2         4/5/91       6/7/91   6.2
      4  2     2         4/6/91       6/8/91   3.2
      5  1     3         5/5/95       6/6/96   3.3
      6  2     3         5/5/97       6/6/98   1.3
      

      2) 使用tidyverse

      split.default(d[-1], cumsum(grepl('Start$', names(d)[-1]))) %>%
        map_dfr(~set_names(., c('DateRangeStart','DateRangeEnd','Value'))) %>% 
        bind_cols(ID = rep(d$ID, nrow(.)/nrow(d)),
                  group = rep(1:(nrow(.)/nrow(d)), each = nrow(d)), .)
      

      3) 使用sjmisc-package:

      library(sjmisc)
      to_long(d, keys = 'group', recode.key = TRUE,
              values = c('DateRangeStart','DateRangeEnd','Value'), 
              c('DateRange1Start','DateRange2Start','DateRange3Start'),
              c('DateRange1End','DateRange2End','DateRange3End'),
              c('Value1','Value2','Value3'))
      

      使用的数据:

      d <- read.table(text = "ID DateRange1Start DateRange1End Value1 DateRange2Start DateRange2End Value2 DateRange3Start DateRange3End Value3
      1 1/1/90 3/1/90 4.4 4/5/91 6/7/91 6.2 5/5/95 6/6/96 3.3
      2 1/2/90 3/2/90 6.1 4/6/91 6/8/91 3.2 5/5/97 6/6/98 1.3", header = TRUE, stringsAsFactors = FALSE)
      

      【讨论】:

        【解决方案6】:

        利用回收:

        data.frame(ID = d[, 1],
                   DateRangeStart = unlist(d[, -1][, c(TRUE, FALSE, FALSE)]),
                   DateRangeEnd  = unlist(d[, -1][, c(FALSE, TRUE, FALSE)]),
                   Value =  unlist(d[, -1][, c(FALSE, FALSE, TRUE)]))
        

        【讨论】:

          【解决方案7】:

          你不需要任何花哨的东西;基本的R 函数就可以了。

          a <- read.table(textConnection("
          ID DateRange1Start DateRange1End Value1 DateRange2Start DateRange2End Value2 DateRange3Start DateRange3End Value3
          1 1/1/90 3/1/90 4.4 4/5/91 6/7/91 6.2 5/5/95 6/6/96 3.3 
          "),header=TRUE)
          b1 <- a[,c(1:4)]; b2 <- a[,c(1,5:7)]; b3 <- a[,c(1,8:10)]
          colnames(b1) <- colnames(b2) <- colnames(b3) <- c("ID","DateRangeStart","DateRangeEnd","Value")
          b <- rbind(b1,b2,b3)
          

          【讨论】:

            猜你喜欢
            • 2013-03-18
            • 2021-10-06
            • 2020-05-01
            • 2021-10-26
            相关资源
            最近更新 更多