web-dev-qa-db-ja.com

変数に列名を付けてRのdata.tableで完全に総称的に機能させるにはどうすればよいですか

まず、@ MattDowleに感謝します。 data.tableは、Rを使い始めて以来、私に起こった最高の出来事の1つです。

2番目: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で複数の列を要約します

そしておそらくもっと私は参照していません。

しかし、上記のすべてのトリックを学び、それらを使用する方法を思い出すためにそれらを調べる必要がなくなったとしても、関数にパラメーターとして渡される列名を操作することは非常に難しいことがわかります面倒な作業。

私が探しているのは、次の回避策/ワークフローの「ベストプラクティスで承認された」代替策です。類似したデータの列がたくさんあり、これらの列またはそれらのセットに対して類似した操作のシーケンスを実行したいと考えます。操作は任意に非常に複雑で、指定された各操作に渡される列名のグループ変数内。

私はこの問題soundsが考案されていることを理解していますが、驚くべき頻度でそれに遭遇します。例は通常非常に乱雑なので、この質問に関連する機能を分離することは困難ですが、ここで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演算とはうまくいかないため、多くを単純化することも、完全に機能することもありません。

52
Philip

あなたが説明している問題は、_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)も変数として渡す必要があります。

上記の理由により、質問に記載されている要件を簡単に達成できるとは思いません。

上記のものや、quote- ingとeval- ingを頻繁に必要とするものよりもシンプルで、エレガントで、短く、設計、実装、または理解が容易です。

他のプログラミング言語と比較すると、ベースRはそのような問題に対処するための非常に便利なツールを提供します。


getmget、_DT[[col_name]]_、parsequoteevalの使用に関する提案はすでにあります。

  • あなたが言及したように、_DT[[col_name]]_は_data.table_最適化ではうまく機能しない可能性があるため、ここではあまり役に立ちません。
  • parseは、文字列を操作するだけで複雑なクエリを作成する最も簡単な方法ですが、基本的な言語構文の検証は行いません。したがって、Rパーサーが受け入れない文字列を解析しようとする可能性があります。さらに、 2655#issuecomment-376781159 に示されているように、セキュリティ上の懸念があります。
  • get/mgetは、このような問題に対処するために最も一般的に提案されているものです。 getおよびmgetは、_[.data.table_によって内部的にキャッチされ、予期される列に変換されます。したがって、任意の複雑なクエリが_[.data.table_によって分解され、期待される列が適切に入力されると想定しています。
  • 数年前にこの質問をされたので、新機能-dot-dot prefix-が最近ロールアウトされています。現在のdata.tableのスコープ外の変数を参照するには、ドットドットを使用して変数名にプレフィックスを付けます。同様に、ファイルシステムで親ディレクトリを参照します。ドットドットの後ろの内部はgetに非常に似ています。接頭辞を持つ変数は_[.data.table_内でde-referencedになります。 。将来のリリースでは、ドットドットプレフィックスで次のような呼び出しが許可される可能性があります。
_col1="a"; col2="b"; col3="g"; col4="x"; col5="y"
DT[..col4==..col5, .(s1=sum(..col1), s2=sum(..col2)), by=..col3]
_
  • 個人的には、代わりにquoteevalを好みます。 quoteevalは、ほとんど最初から手書きで書かれたものとして解釈されます。このメソッドは、列への参照を管理する_data.table_機能に依存しません。すべての最適化が、これらのクエリを手動で書くのと同じように機能することを期待できます。引用符で囲まれた式を印刷して、実際に_data.table_クエリに渡されているものを確認できるため、いつでもデバッグが容易であることがわかりました。さらに、バグが発生するスペースが少なくなります。 R言語オブジェクトを使用して複雑なクエリを作成するのは難しい場合があります。プロシージャを関数にラップして、さまざまなユースケースに適用して簡単に再利用できるようにするのは簡単です。このメソッドは_data.table_から独立していることに注意してください。 R言語の構造を使用します。詳細については、公式の R言語定義言語の計算の章を参照してください。
  • ほかに何か? #1579macroという新しいコンセプトの提案を提出しました。つまり、DT[eval(qi), eval(qj), eval(qby)]のラッパーであるため、R言語オブジェクトを操作する必要があります。そこにコメントを書き込んでください。

例に進みます。すべてのロジックを_do_vars_関数にラップします。 do_vars(donot=TRUE)を呼び出すと、evalではなく_data.table_で計算される式が出力されます。以下のコードは、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]
#}
#
_
5
jangorecki

私はこれを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]
2
geneorama

質問ありがとうございます。元のアプローチは、ほとんどの問題を解決するのに大いに役立ちます。

ここでは、クォート関数を少し調整し、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
logworthy