【问题标题】:R Reshape PerformanceR 重塑性能
【发布时间】:2015-04-18 04:51:54
【问题描述】:

编辑:在创建一个简单的示例 data.frame 时,我为两个 Date 列使用了相同的日期,但事实并非如此,这使得这个问题变得更加困难。

而不是这个数据框:

ID     Date           Balance    Date2        Balance2
1      01-01-2014     10000      01-02-2014   5000
2      01-01-2014     50000      01-02-2014   30000
3      01-01-2014     30000      01-02-2014   15000 
4      01-01-2014     5000       01-02-2014   3500

我有这个数据框:

ID     Date           Balance    Date2        Balance2
1      01-01-2014     10000      01-02-2017   5000
2      01-01-2015     50000      01-02-2016   30000
3      01-08-2014     30000      01-02-2015   15000 
4      01-02-2016     5000       01-02-2018   3500

我想将其重塑为以下内容:

ID     Date           Balance
1      01-01-2014     10000      
1      02-02-2017     5000
2      01-01-2015     50000      
2      01-02-2016     30000      
3      ...            ...        And so on...

我现在有以下内容。

Dates = a character containing all the columns with Dates (Date, Date2, Date3...)
Balances = a character containing all the columns with Balances (Balance1, Balance2...)

df <- reshape(df,
               varying = Balances,
               v.names = "Balance"
               timevar = "Date"
               times = Dates,
               direction = "long")

当我更改示例 data.frame / data.table 时,您提出的出色方法的结果并没有让我得到结果。

主要问题是我在日期列中有不同的日期,我无法更改它。但 Date1 - Date2 - Date3 始终按时间顺序排列。

我需要一种 R 理解它需要获取 Date 列和 Balance 列的方式,将其放在新的 DF 中,然后获取 Date2 和 Balance2,将它们与第一个 DF 绑定,然后是 Date3、Balance3 等等,直到我得到我的 700 个变量。

我正在考虑写一个循环,有什么想法吗?请参阅下面的示例数据。

提前致谢,

罗伯特

df <- data.frame(ID=seq(1:4),
                Date= c("01-01-2014","01-01-2015","01-08-2014","01-02-2016"),
                Balance = c(10000,50000,30000,5000),
                Date2= c("01-02-2017","01-02-2016","01-02-2015","01-02-2018"),
            Balance2 = c(5000,30000,15000,3500))

【问题讨论】:

  • 我无法对您的具体问题发表评论,但我强烈建议您切换到 reshape2,它提供了极大改进的 API,并在 C++ 中实现,具有潜在的显着性能改进。
  • @RobertLuyt,请参阅我的编辑。让我知道它是否适合您。

标签: r performance reshape


【解决方案1】:

如果您的列按照您在示例中提供的名称命名,您可以尝试使用我的“splitstackshape”包中的merged.stack。请注意,“ID”列中的值必须是唯一的才能正常工作(因为它们在您的示例数据中)。

用法很简单:指定变量的“存根”(此处为“日期”和“余额”)。设置sep = "var.stubs" 只会去掉列名的其余部分。 [, .time_1 := NULL] 只是删除在重塑过程中创建的时间列。

library(splitstackshape)
merged.stack(mydf, var.stubs = c("Date", "Balance"), 
             sep = "var.stubs")[, .time_1 := NULL][]
#    ID       Date Balance
# 1:  1 01-01-2014   10000
# 2:  1 01-02-2014    5000
# 3:  2 01-01-2014   50000
# 4:  2 01-02-2014   30000
# 5:  3 01-01-2014   30000
# 6:  3 01-02-2014   15000
# 7:  4 01-01-2014    5000
# 8:  4 01-02-2014    3500

很快(“data.table”的 1.9.8 版)melt 将能够处理转换为半长格式的操作,就像您尝试到这里一样。这将比目前的merged.stack 更快,但merged.stack 应该已经能够处理您目前的情况。

【讨论】:

  • 嗨 Ananda -- 您是否有一些参考资料提到/描述了即将对 data.table::melt() 进行的增强?从头开始:found it!感谢您的提醒。
  • @JoshO'Brien,刚刚收到此评论的 ping。奇怪.... 很高兴您同时找到了参考资料 :-) 说起来可能有点尴尬,但我对这一发展感到兴奋 :-)
  • 哈!这意味着我们至少有两个人...... ;)
【解决方案2】:

如果您关心订单,那么最快的方法可能来自data.table 答案。但如果你不这样做,那么你可以使用rbind 将前三列的行与第一列和最后两列绑定。这将非常快速和简单,但没有您想要的顺序。您可以使用 ID 上的order 函数重新排序。

或者,您可以生成两个矩阵,转置,然后将它们作为向量绑定在一起。这将非常快,因为您只是制作了一些副本和选择,并且重新排序是通过以不同的方式识别数据而不是依赖排序算法来完成的。

dateMat <- as.matrix(df[, c(2, 4)])
balMat  <- as.matrix(df[, c(3, 5)])
dates <- as.vector( t(dateMat) )
balances <- as.vector( t(balMat) )
dfl <- data.frame(ID = rep(df$ID, each = 2), Date = dates, Balance = balances)

您可以在大型 data.frame 上测试这两个版本的速度。

【讨论】:

    【解决方案3】:

    另一个选项可能如下。在dplyr 中使用select,可以同时选择列和更改列名。最后用bind_rows绑定两个数据集。

    数据和代码

    mydf <- structure(list(ID = 1:4, Date = structure(c(1L, 1L, 1L, 1L), .Label = "01-01-2014", class = "factor"), 
    Balance = c(10000L, 50000L, 30000L, 5000L), Date2 = structure(c(1L, 
    1L, 1L, 1L), .Label = "01-02-2014", class = "factor"), Balance2 = c(5000L, 
    30000L, 15000L, 3500L)), .Names = c("ID", "Date", "Balance", 
    "Date2", "Balance2"), class = "data.frame", row.names = c(NA, 
    -4L))
    
    # Convert factor to date object
    mutate_each(mydf, funs(as.Date(., format = "%m-%d-%Y")), Date, Date2) -> mydf
    
    bind_rows(select(mydf, 1:3), select(mydf, 1, Date = Date2, Balance = Balance2)) %>%
    arrange(ID, Date)
    
    #  ID       Date Balance
    #1  1 2014-01-01   10000
    #2  1 2014-01-02    5000
    #3  2 2014-01-01   50000
    #4  2 2014-01-02   30000
    #5  3 2014-01-01   30000
    #6  3 2014-01-02   15000
    #7  4 2014-01-01    5000
    #8  4 2014-01-02    3500
    

    【讨论】:

    • @RobertLuyt 你在这里有各种很棒的方法。尝试所有这些,看看什么最适合你。 :)
    【解决方案4】:

    这是一个data.table 解决方案。我仍在尝试考虑如何删除对第一个 data.table 的调用。

    dt <- structure(list(ID = 1:4, Date = structure(c(1L, 1L, 1L, 1L), .Label = "01-01-2014", class = "factor"), 
    Balance = c(10000L, 50000L, 30000L, 5000L), Date2 = structure(c(1L, 
    1L, 1L, 1L), .Label = "01-02-2014", class = "factor"), Balance2 = c(5000L, 
    30000L, 15000L, 3500L)), .Names = c("ID", "Date", "Balance", 
    "Date2", "Balance2"), class = "data.table", row.names = c(NA, 
    -4L))
    
    dt1 <- melt(dt,id="ID",measure=c("Balance","Balance2"))[, variable := c(as.character(dt$Date), as.character(dt$Date2))]
    dt1
    

    【讨论】:

      【解决方案5】:

      通过组合两个列集来构建新的 data.frame 不是最简单的解决方案吗?这可以在没有reshape 的情况下完成:

      r> x <- data.frame(ID=1:4, Date=as.POSIXct(c('2014-01-01','2014-01-01','2014-01-01','2014-01-01')), Balance=c(10000,50000,30000,5000), Date2=as.POSIXct(c('2014-01-02','2014-01-02','2014-01-02','2014-01-02')), Balance2=c(5000,30000,15000,3500) );
      r> y <- data.frame(ID=c(x$ID,x$ID), Date=c(x$Date,x$Date2), Balance=c(x$Balance,x$Balance2) );
      r> y;
        ID       Date Balance
      1  1 2014-01-01   10000
      2  2 2014-01-01   50000
      3  3 2014-01-01   30000
      4  4 2014-01-01    5000
      5  1 2014-01-02    5000
      6  2 2014-01-02   30000
      7  3 2014-01-02   15000
      8  4 2014-01-02    3500
      

      如果这对您的数据表现良好,您能告诉我吗?

      用于排序:

      r> z <- y[order(y$ID,y$Date),]; rownames(z) <- 1:nrow(z);
      r> z;
        ID       Date Balance
      1  1 2014-01-01   10000
      2  1 2014-01-02    5000
      3  2 2014-01-01   50000
      4  2 2014-01-02   30000
      5  3 2014-01-01   30000
      6  3 2014-01-02   15000
      7  4 2014-01-01    5000
      8  4 2014-01-02    3500
      

      编辑:考虑到您有这么多列,在每个相应的日期和余额列上手动调用 c() 是不切实际的。但是,在玩了一会儿之后,我意识到您可以将names()grep()do.call()c() 函数组合起来,以完全按照您想要的方式自动提取和组合您的数据。您还需要unname() 来删除不需要的元素名称,并需要replicate() 来复制 ID 列足够的次数。

      首先,我想出了一种方法来生成一个用于测试的随机输入 data.frame:

      r> randDate <- function() as.Date('2014-01-01')+as.integer(runif(1,max=30));
      r> randBalance <- function() 5000+as.integer(runif(1,max=18))*5000;
      r> n <- 700;
      r> x <- setNames(do.call(data.frame, c(list(1:4), replicate(n, list(do.call(c, replicate(4, randDate(), simplify=F ) ), do.call(c, replicate(4, randBalance(), simplify=F ) ) ), simplify=F ) ) ), c('ID', sapply(1:n, function(x) c(paste0('Date',x), paste0('Balance',x) ) ) ) );
      r> x;
        ID      Date1 Balance1      Date2 Balance2      Date3 Balance3 ... Balance698    Date699 Balance699    Date700 Balance700
      1  1 2014-01-29    10000 2014-01-08    50000 2014-01-05    40000 ...      30000 2014-01-23      35000 2014-01-08      45000
      2  2 2014-01-30    65000 2014-01-15    10000 2014-01-11    45000 ...      75000 2014-01-29      25000 2014-01-04      50000
      3  3 2014-01-11    75000 2014-01-14    70000 2014-01-24    45000 ...      50000 2014-01-02      10000 2014-01-01      50000
      4  4 2014-01-11    25000 2014-01-11    20000 2014-01-24    20000 ...      50000 2014-01-08      70000 2014-01-11      75000
      

      现在,您可以使用以下方法实现所需的重塑:

      r> y <- data.frame(ID=do.call(c, replicate((ncol(x)-1)/2, x$ID, simplify=F ) ), Date=unname(do.call(c, x[,grep('^Date[0-9]+$', names(x) )] )), Balance=unname(do.call(c, x[,grep('^Balance[0-9]+$', names(x) )] )) );
      r> y;
           ID       Date Balance
      1     1 2014-01-29   10000
      2     2 2014-01-30   65000
      3     3 2014-01-11   75000
      4     4 2014-01-11   25000
      5     1 2014-01-08   50000
      6     2 2014-01-15   10000
      ...
      2795  3 2014-01-02   10000
      2796  4 2014-01-08   70000
      2797  1 2014-01-08   45000
      2798  2 2014-01-04   50000
      2799  3 2014-01-01   50000
      2800  4 2014-01-11   75000
      

      对于订购:

      r> z <- y[order(y$ID,y$Date),]; rownames(z) <- 1:nrow(z);
      r> z;
           ID       Date Balance
      1     1 2014-01-01   55000
      2     1 2014-01-01   20000
      3     1 2014-01-01   15000
      4     1 2014-01-01   75000
      5     1 2014-01-01   40000
      6     1 2014-01-01   85000
      ...
      2795  4 2014-01-30   15000
      2796  4 2014-01-30   65000
      2797  4 2014-01-30    5000
      2798  4 2014-01-30   70000
      2799  4 2014-01-30   35000
      2800  4 2014-01-30   30000
      

      这段代码基本上是立即运行的。速度的关键在于它通过下标 data.frame(例如 x[,grep('^Date[0-9]+$', names(x) )] 用于所有 Date 列)同时提取目标输出列的每个输入列,并通过对 c() 的一次调用运行所有这些列通过对 do.call 的一次调用,它忽略了参数的 data.frame 类,只是将其视为它的底层列表。最终结果是您得到一个输出列的c()-combined 向量,其格式几乎准备好附加到输出 data.frame(您只需删除不需要的元素名称使用unname())。您需要分别对 Date 列和 Balance 列(通过 x[,grep('^Balance[0-9]+$', names(x) )] 下标的 Balance 列)执行此操作,并将它们打包到一个新的 data.frame 构造调用中。唯一的难题是复制输入 ID 列足够多次 ((ncol(x)-1)/2) 以生成与 Date 和 Balance 输出向量对应的正确输出 ID 列。

      这个解决方案是完全矢量化的,没有显式或隐藏的循环。此外,它只使用内置的 R 功能;它不需要依赖任何附加包。我总是尽量避免使用附加包,随着理解代码所需的知识广度增加,这往往会增加复杂性和以后的维护困难。

      【讨论】:

      • 然后你得到提问者要求的你可以通过 ID 订购的东西。这种排序可能有点慢。
      • rbind(DF[, 1:3], DF[, c(1, 4:5)])
      • @Roland,我喜欢你建议的简洁性,但是当我运行它时,我得到了Error in match.names(clabs, names(xi)) : names do not match previous names,而rbind() 似乎没有提供任何指定名称的方法。有没有办法让rbind() 工作?
      • rbind(setNames(DF[, 1:3], c("ID", "Date", "Balance")), setNames(DF[, c(1, 4:5)], c("ID", "Date", "Balance")))
      • 对于这种特殊情况,可以稍微改进为rbind(x[,1:3],setNames(x[,c(1,4:5)],names(x[,1:3])))
      【解决方案6】:

      正如已经提到的in this answerdata.table 的 1.9.6 版(CRAN 2015 年 9 月 19 日)引入了融合到多个列中的能力:

      library(data.table)
      melt(setDT(df), measure.vars = patterns("Date", "Balance"), 
           value.name = c("Date", "Balance"))
      
         ID variable       Date Balance
      1:  1        1 01-01-2014   10000
      2:  2        1 01-01-2015   50000
      3:  3        1 01-08-2014   30000
      4:  4        1 01-02-2016    5000
      5:  1        2 01-02-2017    5000
      6:  2        2 01-02-2016   30000
      7:  3        2 01-02-2015   15000
      8:  4        2 01-02-2018    3500
      

      【讨论】:

        猜你喜欢
        • 2010-12-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-03-10
        • 2017-11-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多