web-dev-qa-db-ja.com

data.tableオブジェクトの関数(プロシージャ)を記述します

本の中でデータ分析のためのソフトウェア:Rを使ったプログラミング、ジョン・チェンバースは、関数は一般的にその副作用のために書かれるべきではないと強調しています。むしろ、関数はその呼び出し環境で変数を変更せずに値を返す必要があります。逆に、data.tableオブジェクトを使用して適切なスクリプトを作成する場合は、通常、関数の結果を格納するために使用される_<-_でのオブジェクト割り当ての使用を特に避ける必要があります。

まず、技術的な質問です。 _proc1_オブジェクトxを引数として受け入れる_data.table_というR関数を想像してみてください(おそらく他のパラメーターに加えて)。 _proc1_はNULLを返しますが、_:=_を使用してxを変更します。私の理解では、_proc1_ proc1(x=x1)を呼び出すと、約束が機能するという理由だけで_x1_のコピーが作成されます。ただし、以下に示すように、元のオブジェクト_x1_は引き続き_proc1_によって変更されます。なぜ/どうですか?

_> require(data.table)
> x1 <- CJ(1:2, 2:3)
> x1
   V1 V2
1:  1  2
2:  1  3
3:  2  2
4:  2  3
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> proc1(x1)
NULL
> x1
   V1 V2 y
1:  1  2 2
2:  1  3 3
3:  2  2 4
4:  2  3 6
> 
_

さらに、proc1(x=x1)を使用することは、xで直接プロシージャを実行するよりも遅くはないようです。これは、promiseの漠然とした理解が間違っており、参照渡しのような方法で機能することを示しています。

_> x1 <- CJ(1:2000, 1:500)
> x1[, paste0("V",3:300) := rnorm(1:nrow(x1))]
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> system.time(proc1(x1))
   user  system elapsed 
   0.00    0.02    0.02 
> x1 <- CJ(1:2000, 1:500)
> system.time(x1[,y:= V1*V2])
   user  system elapsed 
   0.03    0.00    0.03 
_

したがって、data.table引数を関数に渡しても時間が追加されない場合、data.tableの速度と関数の一般化可能性の両方を組み込んだ、data.tableオブジェクトのプロシージャを記述できます。しかし、ジョン・チェンバースが言ったことを考えると、関数には副作用があってはならないということを考えると、このタイプの手続き型プログラミングをRで書くことは本当に「大丈夫」ですか?なぜ彼は副作用が「悪い」と主張したのですか?彼のアドバイスを無視するつもりなら、どのような落とし穴に注意する必要がありますか? 「良い」data.tableプロシージャを作成するにはどうすればよいですか?

37
Michael

はい、_data.table_ sの列の追加、変更、削除はreferenceによって行われます。ある意味では、_data.table_は通常大量のデータを保持しているため、goodのことであり、変更するたびにすべてを再割り当てするのは非常にメモリと時間がかかります。製。一方、Rがデフォルトで_no-side-effect_を使用して促進しようとする_pass-by-value_関数型プログラミングアプローチに反するため、これはbadのことです。副作用のないプログラミングでは、関数を呼び出すときに心配する必要はほとんどありません。入力や環境に影響を与えることはなく、関数の出力に集中することができます。シンプルなので快適です。

もちろん、自分が何をしているのかを知っていれば、ジョン・チェンバースのアドバイスを無視してもかまいません。 「優れた」data.tablesプロシージャの記述について、複雑さと副作用の数を制限する方法として、私があなたである場合に検討するいくつかのルールを次に示します。

  • 関数は複数のテーブルを変更してはなりません。つまり、そのテーブルを変更することが唯一の副作用である必要があります。
  • 関数がテーブルを変更する場合は、そのテーブルを関数の出力にします。もちろん、再割り当てする必要はありません。do.something.to(table)ではなくtable <- do.something.to(table)を実行するだけです。代わりに、関数に別の(「実際の」)出力がある場合、result <- do.something.to(table)を呼び出すときに、出力に注意を向ける方法を想像し、関数の呼び出しが副作用を持っていることを忘れるのは簡単です。テーブル。

「1つの出力/副作用なし」関数はRの標準ですが、上記のルールでは「1つの出力または副作用」が許可されています。副作用が何らかの形で出力の形式であることに同意する場合は、Rの1出力関数型プログラミングスタイルに大まかに固執することで、ルールをあまり曲げないことに同意するでしょう。関数に複数の副作用を持たせることは、もう少しストレッチになります。あなたがそれをすることができないというわけではありませんが、私は可能であればそれを避けようとします。

27
flodel

ドキュメントは改善される可能性がありますが(提案は大歓迎です)、現時点での内容は次のとおりです。おそらく、「関数内でも」と言うべきでしょうか?

?":="で:

data.tablesは、:=、setkey、またはその他のset *関数によって変更時にコピーされることはありません。コピーを参照してください。

DTは参照によって変更され、新しい値が返されます。コピーが必要な場合は、最初にコピーを取ります(DT2 = copy(DT)を使用)。このパッケージは、参照による更新がテーブル全体をコピーするよりも桁違いに高速になる可能性がある(複数列のキーを持つ混合列タイプの)大きなデータ用であることを思い出してください。

そして?copyで(しかし私はこれがsetkeyで混乱していることに気づきます):

入力は参照によって変更され、(目に見えないように)返されるため、複合ステートメントで使用できます。例:setkey(DT、a)[J( "foo")]。コピーが必要な場合は、最初にコピーを取ります(DT2 = copy(DT)を使用)。 copy()は、:=を使用して参照によって列にサブアサインする前に役立つ場合もあります。 ?copyを参照してください。 setattrもパッケージビットに含まれていることに注意してください。どちらのパッケージも、Rの内部setAttrib関数をCレベルで公開するだけですが、戻り値が異なります。 bit :: setattrはNULLを(目に見えないように)返し、関数がその副作用のために使用されていることを通知します。 data.table :: setattrは、複合ステートメントで使用するために、変更されたオブジェクトを(目に見えない形で)返します。

ここで、bit::setattrに関する最後の2つの文は、興味深いことに、flodelのポイント2に関連しています。

これらの関連する質問も参照してください。

data.tableが別のdata.tableへの(のコピーに対して)参照である場合を正確に理解する
参照渡し:data.tableパッケージの:=演算子
data.table 1.8.1。:“ DT1 = DT2”はDT1 = copy(DT2)? と同じではありません

私はあなたの質問のこの部分がとても好きです:

これにより、data.tableの速度と関数の一般化可能性の両方を組み込んだ、data.tableオブジェクトのプロシージャを記述できます。

はい、これは間違いなく意図の1つです。データベースがどのように機能するかを考えてみましょう。データベース内の1つ以上の(大きな)テーブルを参照(挿入/更新/削除)することで、さまざまなユーザー/プログラムが変更されます。これはデータベースランドでは問題なく機能し、data.tableの考え方に似ています。したがって、ホームページ上のsvSocketビデオ、およびinsertdeleteの要望(参照、動詞のみ、副作用関数)。

17
Matt Dowle