【问题标题】:Collating data from a different column per row of a data.table, depending on the text of another column根据另一列的文本,对 data.table 的每行不同列中的数据进行整理
【发布时间】:2012-04-16 14:28:45
【问题描述】:

我想在我的 data.table 中添加一个新列,其中包含来自其他列之一的数据。但是,列的选择因行而异 - 取决于另一列的内容。所以:

对于数据集:

     a_data b_data column_choice
[1,]     55      1             a
[2,]     56      2             a
[3,]     57      3             b

生成者:

dat=data.table(a_data = c(55, 56, 57), 
               b_data = c(1,  2,  3), 
               column_choice = c("a", "a", "b"))

我想要一个新列“chosen”,其中包含(每行)来自“a_data”或“b_data”的数据,具体取决于“column_choice”的值。因此,生成的数据表将是:

     a_data b_data column_choice chosen
[1,]     55      1             a     55
[2,]     56      2             a     56
[3,]     57      3             b      3

我已经设法获得了预期的效果:

dat=dat[, data.table(.SD, chosen=.SD[[paste0(.SD$column_choice, "_data")]]),
        by=1:nrow(a)]
dat$nrow = NULL

但是这感觉很笨拙;也许有一种更简单的方法来做到这一点(这无疑也会教会我一些关于 R 的知识)?

在实践中,数据框还有很多其他列需要保留,除了“a 或 b”还有更多选择,以及要生成的几种类型的列,所以我宁愿不使用基本的ifelse 可能适用于上述基本示例的解决方案。

非常感谢您的帮助。

【问题讨论】:

    标签: r data.table


    【解决方案1】:

    我想我现在找到了一个正确矢量化的衬线,在这种情况下,它也比其他答案更快。

    petesFun2 使用 data.table 聚合作为 petesFun,但现在跨 column_choice 向量化(而不是像以前那样按项目)。

    虽然 petesFun2 适合我的目的,但它确实以不同的顺序保留了行和列。因此,为了与其他答案进行比较,我添加了 petesFun2Clean ,它与其他答案保持相同的顺序。

    petesFun2 <-function(myDat) {
      return(myDat[, cbind(.SD, chosen=.SD[[paste0(.BY$column_choice, "_data")]]),
                   by=column_choice])
    }
    
    petesFun2Clean <-function(myDat) {
      myDat = copy(myDat) # To prevent reference issues
      myDat[, id := seq_len(nrow(myDat))] # Assign an id
      result = myDat[, cbind(.SD, chosen=.SD[[.BY$choice]]),
                     by=list(column_choice, choice=paste0(column_choice, "_data"))]
    
      # recover ordering and column order.
      return(result[order(id), 
                    list(a_data, b_data, c_data, column_choice, chosen)]) 
    }
    
    benchmark(benRes<-   myFun(test.dat),
              petesRes<- petesFun(test.dat),
              dowleRes<- dowleFun(test.dat),
              petesRes2<-petesFun2(test.dat),
              petesRes2Clean<- petesFun2Clean(test.dat),
              replications=25,
              columns=c("test", "replications", "elapsed", "relative"))
    
    #                                         test replications elapsed  relative
    # 1                  benRes <- myFun(test.dat)           25   0.337  4.160494
    # 3             dowleRes <- dowleFun(test.dat)           25   0.191  2.358025
    # 5 petesRes2Clean <- petesFun2Clean(test.dat)           25   0.122  1.506173
    # 4           petesRes2 <- petesFun2(test.dat)           25   0.081  1.000000
    # 2             petesRes <- petesFun(test.dat)           25   4.018 49.604938
    
    identical(petesRes2, benRes)
    # FALSE (due to row and column ordering)
    identical(petesRes2Clean, benRes)
    # TRUE
    

    编辑:我刚刚注意到(正如 Matthew 在 cmets 中提到的)我们现在按组 :=。所以我们可以放弃 cbind 并简单地做:

    myDat[, 选择 := .SD[[paste0(.BY$column_choice, "_data")]], by=column_choice]

    【讨论】:

    • 噢!很高兴按 column_choice 分组,+1。必须有一种方法可以避免cbind() 并进一步减少时间。实施时,:= 按组的出色测试用例。
    • 按组使用:= 进行很好的编辑。理想情况下,我们希望避免使用.SD 来提高效率(以节省使用每个组不需要的所有列填充.SD)。也许:myDat[, chosen:=myDat[[paste0(column_choice,"_data")]][.I], by=column_choice]。如果这行得通,随着myDat 的列数增加,它应该会明显更快。
    【解决方案2】:

    当我想到笨重时,我会想到旧自行车或旧汽车之类的东西,但也可以通过迭代行来在 R 中做事。因此,下面的内容看起来比您在问题中发布的内容更笨拙,但它以我认为更矢量化的方式寻求解决方案。以下似乎比您在上面发布的更时尚的代码快约 10 倍(并返回相同的结果)。

    此建议依赖于reshape2 包:

    library(data.table)
    library(reshape2)
    

    我添加了“c”作为可能的column_choice,让事情变得更有趣:

    dat=data.table(a_data = c(55,56,57,65), 
      b_data = c(1,2,3,4),c_data=c(1000,1001,1002,1003),
      column_choice = c("a", "c", "a", "b"))
    

    以下是步骤,包装在一个函数中,为基准测试做准备。

    myFun<-function(myDat){
    # convert data.table to data.frame for melt()ing
      dat1<-data.frame(myDat)
    # add ID variable to keep track of things
      dat1$ID<-seq_len(nrow(dat1))
    # melt data - because of this line, it's important to only
    # pass those variables that are used to select the appropriate value
    # i.e., a_data,b_data,c_data,column_choice
      dat2<-melt(dat1,id.vars=c("ID","column_choice"))
    # Determine which value to choose: a, b, or c
      dat2$chosen<-as.numeric(dat2$column_choice==substr(dat2$variable,
        1,1))*dat2$value
    # cast the data back into the original form
      dat_cast<-dcast(dat2,ID+column_choice~.,
        fun.aggregate=sum,value.var="chosen")
    # rename the last variable
      names(dat_cast)[ncol(dat_cast)]<-"chosen"
    # merge data back together and return results as a data.table
      datOUT<-merge(dat1,dat_cast,by=c("ID","column_choice"),sort=FALSE)
      return(data.table(datOUT[,c(names(myDat),"chosen")]))
    }
    

    这是你的解决方案打包成一个函数:

    petesFun<-function(myDat){
      datOUT=myDat[, data.table(.SD,
        chosen=.SD[[paste0(.SD$column_choice, "_data")]]),
        by=1:nrow(myDat)]
      datOUT$nrow = NULL
      return(datOUT)
    }
    

    这看起来比myFun 优雅得多。然而,基准测试结果显示出很大的差异:

    制作更大的data.table:

    test.df<-data.frame(lapply(dat,rep,100))
    test.dat<-data.table(test.df)
    

    和基准测试:

    library(rbenchmark)
    
    benchmark(myRes<-myFun(test.dat),petesRes<-petesFun(test.dat),
     replications=25,columns=c("test", "replications", "elapsed", "relative"))
    #                             test replications elapsed relative
    # 1       myRes <- myFun(test.dat)           25   0.412  1.00000
    # 2 petesRes <- petesFun(test.dat)           25   5.429 13.17718
    
    identical(myRes,petesRes)
    # [1] TRUE
    

    我建议可以用不同的方式解释“笨拙”:)

    【讨论】:

    • 附注我不能融化 data.table 吗?啊,显然 1.8.1 会解决这个问题。
    • 非常感谢您。我一直在寻求了解融化,这是一个很大的帮助。如果性能变得重要,我当然会考虑这种方法。
    • 但是在提出这个问题时,我想知道是否有一些非常简单的选项(也许是一条优雅的线)允许以矢量化方式选择不同的列。也许这种东西不存在?
    • @Peter,我也对优雅的单线感兴趣! (我会留意这里。)
    • 如果你有兴趣,我已经发布了一个更快的方法来做到这一点,只使用一个(ish)data.table 操作。为融化的例子干杯!
    【解决方案3】:

    我们开始越来越多地使用for 循环来处理data.table 的此类任务。基于 Ben 的回答并使用他的基准,以下内容如何:

    dowleFun = function(DT) {
      DT = copy(DT)   # Faster to remove this line to add column by reference, but  
                      # included copy() because benchmark repeats test 25 times and
                      # the other tests use the same input table
      w = match(paste0(DT$column_choice,"_data"),names(DT))
      DT[,chosen:=NA_real_]    # allocate new column (or clear it if already exists)
      j = match("chosen",names(DT))     
      for (i in 1:nrow(DT))
          set(DT,i,j,DT[[w[i]]][i])
      DT
    }
    
    benchmark(benRes<-myFun(test.dat),
        petesRes<-petesFun(test.dat),
        dowleRes<-dowleFun(test.dat),
        replications=25,columns=c("test", "replications", "elapsed", "relative"),
        order="elapsed")
    
    #                            test replications elapsed relative
    # 3 dowleRes <- dowleFun(test.dat)           25    0.30      1.0
    # 1      benRes <- myFun(test.dat)           25    0.39      1.3
    # 2 petesRes <- petesFun(test.dat)           25    5.79     19.3
    

    如果您可以删除 copy(),那么它应该会更快,并且可以更好地扩展到更大的数据集。为了测试这一点,也许可以创建一个非常大的表并计算一次运行需要多长时间。

    在这种情况下,一个简单的for 循环会更容易理解。

    话虽如此,如果 i 可以是 2 列 matrix,则可以使用 base 中的 A[B] 语法(其中 B 包含要选择的行和列位置)并且它是单行:

    DT[,chosen:=DT[cbind(1:nrow(DT),paste0(column_choice,"_data"))]]
    

    目前你得到这个:

    > DT[cbind(1:3,c(4,4,5))]
    Error in `[.data.table`(test.dat, cbind(1:3, c(4, 4, 5))) : 
      i is invalid type (matrix). Perhaps in future a 2 column matrix could return
      a list of elements of DT (in the spirit of A[B] in FAQ 2.14). Please let
      maintainer('data.table') know if you'd like this, or add your comments to
      FR #1611.
    

    【讨论】:

    • 谢谢 - 非常有趣的解决方案,我没有意识到 data.table 可以通过引用传递(虽然我还不完全理解引用是如何工作的,但这似乎取决于你是否在我的初始测试中是否使用了除 := 以外的任何转换?)。
    • @Peter 不知道你在这里问什么。 ?":="?copy 和他们的示例部分有帮助吗?还要在 data.table 标记中搜索“reference”或“:="。
    • 我现在意识到,在newDT = DT 之后,newDT[2, col1:= 5] 也会影响DT,因为操作是通过引用进行的。但是,如果在这两个操作之间我执行其他操作,例如newDT$col2[2]=5,那么之后,对newDT 的更改(即使是使用:= 的更改)也不再反映在DT 中。那么newDT$col2[2]=5 是否会以某种方式破坏参考?也许这应该是一个单独的问题......
    • @Peter 好问题。是的,请提出新问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-08-12
    • 2021-10-04
    • 1970-01-01
    • 1970-01-01
    • 2020-01-11
    • 2022-01-03
    • 1970-01-01
    相关资源
    最近更新 更多