如何在R中的data.table中完全使用变量中的列名

时间:2014-07-18 20:26:00

标签: r data.table calculated-columns programmatically-created

首先:感谢@MattDowle; data.table是最好的事情之一 自从我开始使用R以来,曾经发生在我身上。

第二:我知道变量列的各种用例的许多变通方法 data.table中的姓名,包括:

  1. Select / assign to data.table variables which names are stored in a character vector
  2. pass column name in data.table using variable in R
  3. Referring to data.table columns by names saved in variables
  4. passing column names to data.table programmatically
  5. Data.table meta-programming
  6. How to write a function that calls a function that calls data.table?
  7. Using dynamic column names in `data.table`
  8. dynamic column names in data.table, R
  9. Assign multiple columns using := in data.table, by group
  10. Setting column name in "group by" operation with data.table
  11. R summarizing multiple columns with data.table
  12. 可能还有更多我没有引用。

    但是:即使我学会了上面记录的所有技巧,我也是如此 从来没有看过他们提醒自己如何使用它们,我仍然会发现 使用作为参数传递给函数的列名称 这是一项非常乏味的任务。

    我正在寻找的是“最佳实践认可”替代方案 到以下变通方法/工作流程。考虑 我有一堆类似数据的列,并希望对这些列或它们的集合执行一系列类似的操作,其中操作具有任意高的复杂性,并且列名称组传递给指定的每个操作一个变量。

    我意识到这个问题是听起来,但我却以惊人的频率遇到它。这些例子通常非常混乱,很难找出与这个问题相关的功能,但我最近偶然发现了一个相当简单的简化用作MWE的方法:

    library(data.table)
    library(lubridate)
    library(zoo)
    
    the.table <- data.table(year=1991:1996,var1=floor(runif(6,400,1400)))
    the.table[,`:=`(var2=var1/floor(runif(6,2,5)),
                    var3=var1/floor(runif(6,2,5)))]
    
    # Replicate data across months
    new.table <- the.table[, list(asofdate=seq(from=ymd((year)*10^4+101),
                                               length.out=12,
                                               by="1 month")),by=year]
    
    # Do a complicated procedure to each variable in some group.
    var.names <- c("var1","var2","var3")
    
    for(varname in var.names) {
        #As suggested in an answer to Link 3 above
        #Convert the column name to a 'quote' object
        quote.convert <- function(x) eval(parse(text=paste0('quote(',x,')')))
    
        #Do this for every column name I'll need
        varname <- quote.convert(varname)
        anntot <- quote.convert(paste0(varname,".annual.total"))
        monthly <- quote.convert(paste0(varname,".monthly"))
        rolling <- quote.convert(paste0(varname,".rolling"))
        scaled <- quote.convert(paste0(varname,".scaled"))
    
        #Perform the relevant tasks, using eval()
        #around every variable columnname I may want
        new.table[,eval(anntot):=
                   the.table[,rep(eval(varname),each=12)]]
        new.table[,eval(monthly):=
                   the.table[,rep(eval(varname)/12,each=12)]]
        new.table[,eval(rolling):=
                   rollapply(eval(monthly),mean,width=12,
                             fill=c(head(eval(monthly),1),
                                    tail(eval(monthly),1)))]
        new.table[,eval(scaled):=
                   eval(anntot)/sum(eval(rolling))*eval(rolling),
                  by=year]
    }
    

    当然,这里对数据和变量的特殊影响是无关紧要的,所以请不要关注它或建议改进以完成它在这种特定情况下所完成的工作。相反,我正在寻找的是一种通用策略,用于将data.table动作的任意复杂过程重复应用于列列表或列表列表,在变量中指定或传递为函数的参数,其中过程必须以编程方式引用变量/参数中指定的列,并且可能包括更新,联接,分组,对data.table特殊对象.I的调用,{{1}等等;但是比上面的那个或者需要频繁.SDquote - 的其他更简单,更优雅,更短或更容易设计,实现或理解的一个。

    请特别注意,由于这些过程相当复杂并且涉及重复更新eval然后引用更新的列,因此标准data.table方法通常不是可行的替代方法。同时将lapply(.SD,...), ... .SDcols = ...的每次调用替换为eval(a.column.name)也不会简化太多,也不会完全有效,因为据我所知,这与其他DT[[a.column.name]]操作不相称。

3 个答案:

答案 0 :(得分:4)

您描述的问题与data.table并不严格相关。
复杂的查询无法轻松地转换为机器可以解析的代码,因此,我们无法为复杂的操作编写查询而摆脱复杂性。
试想一下如何以编程方式构造以下data.table查询的查询

DT[, c(f1(v1, v2, opt=TRUE),
       f2(v3, v4, v5, opt1=FALSE, opt2=TRUE),
       lapply(.SD, f3, opt1=TRUE, opt2=FALSE))
   , by=.(id1, id2)]

使用dplyr SQL -假设所有列(id1,id2,v1 ... v5)或什至选项(opt,opt1,opt2)都应作为变量传递。 / p>

由于上述原因,我认为您无法轻松完成问题中所述的要求:

  

比上面的一个或多个需要频繁quote-ing和eval-ing的设计更简单,更优雅,更短或更易于设计,实施或理解。

尽管与其他编程语言相比,base R提供了非常有用的工具来处理此类问题。


您已经找到使用getmgetDT[[col_name]]parsequoteeval的建议。

  • 正如您提到的,DT[[col_name]]data.table优化中可能无法很好地发挥作用,因此在这里没有太大用处。
  • parse可能是构造复杂查询的最简单方法,因为您只能对字符串进行操作,但是它不提供基本的语言语法验证。因此,您可能最终尝试解析R解析器不接受的字符串。此外,还有2655#issuecomment-376781159中提出的安全问题。
  • get / mget是解决此类问题的最常用建议。 getmget在内部被[.data.table捕获并转换为预期的列。因此,您假设[.data.table可以分解任意复杂的查询,并且可以正确输入期望的列。
  • 自从几年前您问这个问题以来,新功能-点-点前缀-最近已经推出。您可以使用点号为变量名添加前缀,以引用当前data.table范围之外的变量。与您在文件系统中引用父目录类似。点到点后面的内部结构与get非常相似,具有前缀的变量将在[.data.table内部进行去引用。 。在将来的版本中,点号前缀可能会允许以下调用:
col1="a"; col2="b"; col3="g"; col4="x"; col5="y"
DT[..col4==..col5, .(s1=sum(..col1), s2=sum(..col2)), by=..col3]
  • 我个人更喜欢使用quoteevalquoteeval的解释几乎是从头开始的。此方法不依赖data.table功能来管理对列的引用。我们可以预期所有优化的工作方式都与您手工编写这些查询的方式相同。我发现调试起来也更容易,因为您可以随时打印带引号的表达式以查看实际传递给data.table查询的内容。此外,发生错误的空间更少。使用R语言对象构造复杂的查询有时很棘手,很容易将过程包装为函数,因此可以将其应用于不同的用例并易于重用。请注意,此方法独立于data.table。它使用R语言构造。您可以在计算语言一章中的官方R Language Definition中找到有关此信息的更多信息。
  • 还有什么?我在#1579中提出了一个名为 macro 的新概念的建议。简而言之,它是DT[eval(qi), eval(qj), eval(qby)]的包装器,因此您仍然必须对R语言对象进行操作。欢迎您在此处发表评论。

转到示例。我将所有逻辑包装到do_vars函数中。调用do_vars(donot=TRUE)将打印要在data.table而不是eval上计算的表达式。以下代码应在OP代码之后运行。

expected = copy(new.table)
new.table = the.table[, list(asofdate=seq(from=ymd((year)*10^4+101), length.out=12, by="1 month")), by=year]

do_vars = function(x, y, vars, donot=FALSE) {
  name.suffix = function(x, suffix) as.name(paste(x, suffix, sep="."))
  do_var = function(var, x, y) {
    substitute({
      x[, .anntot := y[, rep(.var, each=12)]]
      x[, .monthly := y[, rep(.var/12, each=12)]]
      x[, .rolling := rollapply(.monthly, mean, width=12, fill=c(head(.monthly,1), tail(.monthly,1)))]
      x[, .scaled := .anntot/sum(.rolling)*.rolling, by=year]
    }, list(
      .var=as.name(var),
      .anntot=name.suffix(var, "annual.total"),
      .monthly=name.suffix(var, "monthly"),
      .rolling=name.suffix(var, "rolling"),
      .scaled=name.suffix(var, "scaled")
    ))
  }
  ql = lapply(setNames(nm=vars), do_var, x, y)
  if (donot) return(ql)
  lapply(ql, eval.parent)
  invisible(x)
}
do_vars(new.table, the.table, c("var1","var2","var3"))
all.equal(expected, new.table)
#[1] TRUE
do_vars(new.table, the.table, c("var1","var2","var3"), donot=TRUE)
#$var1
#{
#    x[, `:=`(var1.annual.total, y[, rep(var1, each = 12)])]
#    x[, `:=`(var1.monthly, y[, rep(var1/12, each = 12)])]
#    x[, `:=`(var1.rolling, rollapply(var1.monthly, mean, width = 12, 
#        fill = c(head(var1.monthly, 1), tail(var1.monthly, 1))))]
#    x[, `:=`(var1.scaled, var1.annual.total/sum(var1.rolling) * 
#        var1.rolling), by = year]
#}
#
#$var2
#{
#    x[, `:=`(var2.annual.total, y[, rep(var2, each = 12)])]
#    x[, `:=`(var2.monthly, y[, rep(var2/12, each = 12)])]
#    x[, `:=`(var2.rolling, rollapply(var2.monthly, mean, width = 12, 
#        fill = c(head(var2.monthly, 1), tail(var2.monthly, 1))))]
#    x[, `:=`(var2.scaled, var2.annual.total/sum(var2.rolling) * 
#        var2.rolling), by = year]
#}
#
#$var3
#{
#    x[, `:=`(var3.annual.total, y[, rep(var3, each = 12)])]
#    x[, `:=`(var3.monthly, y[, rep(var3/12, each = 12)])]
#    x[, `:=`(var3.rolling, rollapply(var3.monthly, mean, width = 12, 
#        fill = c(head(var3.monthly, 1), tail(var3.monthly, 1))))]
#    x[, `:=`(var3.scaled, var3.annual.total/sum(var3.rolling) * 
#        var3.rolling), by = year]
#}
#

答案 1 :(得分:2)

我试着在data.table思考中做到这一点&#34;这不是很糟糕......但是在经历了一段尴尬的时间后,我放弃了。 Matt说像片断然后加入&#39 ;,但是我无法找到做这些片段的优雅方法,特别是因为最后一个取决于之前的步骤。

我不得不说,这是一个非常出色的问题,我也经常遇到类似的问题。我喜欢data.table,但有时我仍然很挣扎。我不知道我是否在努力解决数据问题或问题的复杂性。

这是我采取的不完整的方法。

实际上我可以想象,在正常的过程中,你会有更多的中间变量存储,这对计算这些值很有用。

library(data.table)
library(zoo)

## Example yearly data
set.seed(27)
DT <- data.table(year=1991:1996,
                 var1=floor(runif(6,400,1400)))
DT[ , var2 := var1 / floor(runif(6,2,5))]
DT[ , var3 := var1 / floor(runif(6,2,5))]
setkeyv(DT,colnames(DT)[1])
DT

## Convenience function
nonkey <- function(dt){colnames(dt)[!colnames(dt)%in%key(dt)]}

## Annual data expressed monthly
NewDT <- DT[, j=list(asofdate=as.IDate(paste(year, 1:12, 1, sep="-"))), by=year]
setkeyv(NewDT, colnames(NewDT)[1:2])

## Create annual data
NewDT_Annual <- NewDT[DT]
setnames(NewDT_Annual, 
         nonkey(NewDT_Annual), 
         paste0(nonkey(NewDT_Annual), ".annual.total"))

## Compute monthly data
NewDT_Monthly <- NewDT[DT[ , .SD / 12, keyby=list(year)]]
setnames(NewDT_Monthly, 
         nonkey(NewDT_Monthly), 
         paste0(nonkey(NewDT_Monthly), ".monthly"))

## Compute rolling stats
NewDT_roll <- NewDT_Monthly[j = lapply(.SD, rollapply, mean, width=12, 
                                       fill=c(.SD[1],tail(.SD, 1))),
                            .SDcols=nonkey(NewDT_Monthly)]
NewDT_roll <- cbind(NewDT_Monthly[,1:2,with=F], NewDT_roll)
setkeyv(NewDT_roll, colnames(NewDT_roll)[1:2])
setnames(NewDT_roll, 
         nonkey(NewDT_roll), 
         gsub(".monthly$",".rolling",nonkey(NewDT_roll)))

## Compute normalized values

## Compute "adjustment" table which is 
## total of each variable, by year for rolling
## divided by
## original annual totals

## merge "adjustment values" in with monthly data, and then 
## make a modified data.table which is each varaible * annual adjustment factor

## Merge everything
NewDT_Combined <- NewDT_Annual[NewDT_roll][NewDT_Monthly]

答案 2 :(得分:1)

感谢您的提问。您最初的方法在解决大多数问题方面都有很长的路要走。

这里我稍微调整了引用函数,并改变了解析方法并将整个RHS表达式作为字符串而不是单个变量进行评估。

理由是:

  • 您可能不希望通过声明在循环开始时需要使用的每个变量来重复自己。
  • 字符串将更好地扩展,因为它们可以通过编程方式生成。我在下面添加了一个示例,用于计算行数百分比来说明这一点。
library(data.table)
library(lubridate)
library(zoo)

set.seed(1)
the.table <- data.table(year=1991:1996,var1=floor(runif(6,400,1400)))
the.table[,`:=`(var2=var1/floor(runif(6,2,5)),
                var3=var1/floor(runif(6,2,5)))]

# Replicate data across months
new.table <- the.table[, list(asofdate=seq(from=ymd((year)*10^4+101),
                                           length.out=12,
                                           by="1 month")),by=year]
# function to paste, parse & evaluate arguments
evalp <- function(..., envir=parent.frame()) {eval(parse(text=paste0(...)), envir=envir)}

# Do a complicated procedure to each variable in some group.
var.names <- c("var1","var2","var3")

for(varname in var.names) {

  # 1. For LHS, use paste0 to generate new column name as string (from @eddi's comment)
  # 2. For RHS, use evalp
  new.table[, paste0(varname, '.annual.total') := evalp(
    'the.table[,rep(', varname, ',each=12)]'
  )]

  new.table[, paste0(varname, '.monthly') := evalp(
    'the.table[,rep(', varname, '/12,each=12)]'
  )]

  # Need to add envir=.SD when working within the table
  new.table[, paste0(varname, '.rolling') := evalp(
    'rollapply(',varname, '.monthly,mean,width=12, 
        fill=c(head(', varname, '.monthly,1), tail(', varname, '.monthly,1)))'
    , envir=.SD
  )]

  new.table[,paste0(varname, '.scaled'):= evalp(
      varname, '.annual.total / sum(', varname, '.rolling) * ', varname, '.rolling'
      , envir=.SD
    )
    ,by=year
  ]

  # Since we're working with strings, more freedom 
  # to work programmatically
  new.table[, paste0(varname, '.row.percent') := evalp(
    'the.table[,rep(', varname, '/ (', paste(var.names, collapse='+'), '), each=12)]'
  )]
}