【问题标题】:How can one work fully generically in data.table in R with column names in variables如何在 R 中的 data.table 中使用变量中的列名完全通用地工作
【发布时间】:2014-09-10 01:52:48
【问题描述】:

首先:感谢@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

可能还有更多我没有参考的内容。

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

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

我意识到这个问题听起来是人为的,但我却以惊人的频率遇到它。这些示例通常非常混乱,以至于很难分离出与此问题相关的功能,但我最近偶然发现了一个相当简单的示例,可以在此处用作 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 = ... 方法通常不是可行的替代品。同样,用DT[[a.column.name]] 替换eval(a.column.name) 的每个调用既不能简化很多,也不能完全正常工作,因为据我所知,这与其他data.table 操作不兼容。

【问题讨论】:

  • 不太确定你在找什么,因为那个例子远非最小的 imo,但我通常在 RHS 上使用get(varname)(其中varname 是例如“var1”而不是引用表达式),对于:= 的 LHS,您可以简单地执行以下操作:dt[, paste0(varname, '.rolling') := ...]
  • 公平点,虽然很难知道如何“最小化”做出一个明确应该表明问题仅在程序复杂/涉及许多步骤时才相关的示例。关于get(),我考虑过,但stackoverflow.com/a/12392269/241643 暗示它是次优的。现在不是这样了吗?
  • 理论上,如果您有大量列,get 可能不是最理想的,但在实践中我发现它更容易使用(这通常意味着更快的整体运行时间,当您包括编写时间/理解/维护代码)。在某些时候,它可能会被优化为与eval(quote 一样高效。我还记得有一个 FR 要求将. 实现为j-表达式中的一个函数,该函数相当于get,但效率很高(它还包括使用.. 作为访问外部变量的函数本地范围)。
  • IIUC 你所要求的功能似乎太笼统了..涵盖了 data.table 的许多功能并处理任何复杂的操作..
  • 从阅读 adv-r.had.co.nz/Expressions.html 开始 - 你的 quote.convert() 只是 as.name()

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


【解决方案1】:

您描述的问题与data.table 没有严格关系。
复杂的查询不能轻易地翻译成机器可以解析的代码,因此我们无法避免为复杂的操作编写查询时的复杂性。
您可以尝试想象如何使用dplyr 或SQL 以编程方式为以下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)]

假设所有列(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 是处理此类问题的最常用建议。 get 和 mget 在内部被 [.data.table 捕获并转换为预期的列。因此,您假设您的任意复杂查询将能够被 [.data.table 分解并正确输入预期的列。
  • 自从您在几年前提出这个问题以来,最近推出了新功能 - dot-dot prefix。您使用点-点为变量名称添加前缀以引用当前 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]
  • 我个人更喜欢quote 和eval。 quote 和 eval 几乎被解释为从头开始手写。此方法不依赖data.table 管理对列的引用的能力。我们可以期望所有优化的工作方式与您手动编写这些查询的方式相同。我发现它也更容易调试,因为您可以随时打印带引号的表达式来查看实际传递给data.table 查询的内容。此外,发生错误的空间更小。使用 R 语言对象构建复杂的查询有时很棘手,很容易将过程包装到函数中,因此它可以应用于不同的用例并易于重用。需要注意的是,此方法独立于data.table。它使用 R 语言结构。您可以在计算语言章节的官方R Language Definition 中找到更多信息。

  • 还有什么?

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

转到示例。下面我将展示两种解决方法。第一个将使用基本 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]
#}
#
  • 提议的 data.table 元编程
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]
}

【讨论】:

  • 嗨@jangorecki!也许用指向data.table v1.14.1devel 的指针更新这个不错的答案,第 10 项:“添加了用于在 data.table 上编程的新接口”干杯
【解决方案2】:

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

在这里我稍微调整了引用函数,并改变了将整个 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)]'
  )]
}

【讨论】:

  • 不知道为什么人们不赞成这个;也许他们可以提供一些反馈?我的想法:1. 这在可扩展的意义上“有效”,2. 它类似于我使用的一些方法,因为缺乏更好的方法来让 R 重复我想要的内容,我基本上不得不编写代码编写 R 代码(这相当于对一些细节取模),以及 3. 因此它是危险的/非标准的/不是超级优雅的。然而,到目前为止,还没有人提供一种规范的、有效的方法,它不使用一些类似的精神黑客。在那之前 +1,但很高兴收到任何有更好答案的人的来信。
【解决方案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]

【讨论】:

  • 这很有趣;谢谢。我刚刚花时间完成它并了解工作流程。这对我来说并不自然,但我理解这个想法。我不确定是因为它对我使用data.table 的方式来说是新的/陌生的,还是因为它实际上是曲折的/真正不自然的。诚然,可能没有自然的方式来做我(/我们)正在尝试做的事情。我希望更多的人可以留下他们的想法;如果这在一段时间内没有发生,我会接受或者可能在赏金之后。谢谢!
  • 菲利普,我真的很感谢你的 cmets。以我的经验,采用 DT 非常不自然,但非常值得。就像在 Excel 上使用 R 一样。我在 SO 上看到让我大吃一惊的例子(通常来自 Matt 和 Arun),我想知道我是否做得对。
  • 另外,我认为您不应该按原样接受这个答案。
猜你喜欢
  • 2016-03-30
  • 2022-01-15
  • 1970-01-01
  • 2017-11-04
  • 2017-10-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-16
相关资源
最近更新 更多