63

首先:感谢@MattDowle;data.table是自从我开始使用R.

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

  1. 选择/分配给 data.table 哪些名称存储在字符向量中的变量
  2. 使用 R 中的变量在 data.table 中传递列名
  3. 通过保存在变量中的名称引用 data.table 列
  4. 以编程方式将列名传递给 data.table
  5. Data.table 元编程
  6. 如何编写调用调用data.table的函数的函数?
  7. 在 `data.table` 中使用动态列名
  8. data.table 中的动态列名,R
  9. 使用 := 在 data.table 中按组分配多列
  10. 使用 data.table 在“分组依据”操作中设置列名
  11. R用data.table总结多列

可能还有更多我没有参考。

但是:即使我学会了上面记录的所有技巧,以至于我不必查看它们来提醒自己如何使用它们,我仍然会发现使用作为参数传递给函数的列名是一种极其繁琐的任务。

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

我意识到这个问题听起来很做作,但我以惊人的频率遇到它。这些示例通常非常混乱,以至于很难分离出与该问题相关的功能,但我最近偶然发现了一个相当简单的示例,可以在这里简化为 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的调用.SD等;但是一种比上述或其他需要频繁使用的更简单、更优雅、更短或更容易设计、实现或理解的quote-ing 和eval-ing。

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

4

3 回答 3

12

您描述的问题与data.table.
复杂的查询不能轻易地转换为机器可以解析的代码,因此我们无法避免为复杂操作编写查询的复杂性。
您可以尝试想象如何data.table使用dplyrSQL以编程方式为以下查询构造查询:

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)]

假设所有列(id1, id2, v1... v5)甚至选项(opt, opt1, opt2)都应该作为变量传递。

由于查询表达的复杂性,我认为您无法轻松完成问题中所述的要求:

比上述或其他需要频繁quote-ing 和eval-ing 的方法更简单、更优雅、更短或更容易设计、实现或理解。

虽然与其他编程语言相比,base R 提供了非常有用的工具来处理这些问题。


您已经找到了使用get, mget, DT[[col_name]], parse, quote, 的建议eval

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

  • 还有什么?

    • 我在#1579中提交了一个名为的新概念的提案。简而言之,它是一个包装器,因此您仍然必须对 R 语言对象进行操作。欢迎您在那里发表您的评论。DT[eval(qi), eval(qj), eval(qby)]
    • 最近我在PR#4304中提出了另一种元编程接口的方法。简而言之,它将基本 Rsubstitute功能插入到[.data.table使用新参数env中。

转到示例。下面我将展示两种解决方法。第一个将使用基本 R 元编程,第二个将使用PR#4304中提出的 data.table 元编程(见上文)。

  • 基于语言的 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]
#}
#
  • 提出的数据表元编程
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]

name.suffix = function(x, suffix) as.name(paste(x, suffix, sep="."))
do_var2 = function(var, x, y) {
  x[, .anntot := y[, rep(.var, each=12)],
    env = list(
      .anntot = name.suffix(var, "annual.total"),
      .var = var
    )]
  x[, .monthly := y[, rep(.var/12, each=12)],
    env = list(
      .monthly = name.suffix(var, "monthly"),
      .var = var
    )]
  x[, .rolling := rollapply(.monthly, mean, width=12, fill=c(head(.monthly,1), tail(.monthly,1))),
    env = list(
      .rolling = name.suffix(var, "rolling"),
      .monthly = name.suffix(var, "monthly")
    )]
  x[, .scaled := .anntot/sum(.rolling)*.rolling, by=year,
    env = list(
      .scaled = name.suffix(var, "scaled"),
      .anntot = name.suffix(var, "annual.total"),
      .rolling = name.suffix(var, "rolling")
    )]
  TRUE
}

sapply(setNames(nm=var.names), do_var2, new.table, the.table)
#var1 var2 var3 
#TRUE TRUE TRUE 
all.equal(expected, new.table)
#[1] TRUE

数据和更新的 OP 代码

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[,paste0(varname,".annual.total"):=
              the.table[,rep(eval(varname),each=12)]]
  new.table[,paste0(varname,".monthly"):=
              the.table[,rep(eval(varname)/12,each=12)]]
  new.table[,paste0(varname,".rolling"):=
              rollapply(eval(monthly),mean,width=12,
                        fill=c(head(eval(monthly),1),
                               tail(eval(monthly),1)))]
  new.table[,paste0(varname,".scaled"):=
              eval(anntot)/sum(eval(rolling))*eval(rolling),
            by=year]
}
于 2019-02-21T05:51:12.767 回答
4

谢谢你的问题。您最初的方法对解决大多数问题大有帮助。

在这里,我稍微调整了引用函数,并更改了将整个 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)]'
  )]
}
于 2015-11-09T04:58:21.163 回答
3

我试图在 data.table 中做到这一点,认为“这还不错”......但经过一段令人尴尬的时间后,我放弃了。马特说“分块做然后加入”,但我想不出优雅的方法来完成这些部分,特别是因为最后一个取决于前面的步骤。

我不得不说,这是一个非常巧妙的问题,我也经常遇到类似的问题。我喜欢 data.table,但有时我仍然很挣扎。我不知道我是否在为 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]
于 2014-09-17T23:14:05.470 回答