完璧なdata.frameなどといったものは存在しない。完璧な絶望が存在しないようにね。

tidyポエム Advent Calendar 2017 - Adventar 5日目の記事です。

なんかdata.frameとtibbleについての議論が盛り上がってるようなので、data.frameについて書きましょう。 ええ、決して「tidygraphに触ってみる」なんていうタイトルでアドベントカレンダーに登録してたけどついさっきまで忘れていた、みたいなことではないですよ。 時流を捉えたテーマを選んだまでです。ええ。

I()ゆえに人は苦しまねばならぬ!!

そもそも、data.frame()でリストを渡したときというのはどういう挙動が意図されているのでしょう。?data.frameを見てみましょう。

data.frame converts each of its arguments to a data frame by calling as.data.frame(optional = TRUE). As that is a generic function, methods can be written to change the behaviour of arguments according to their classes: R comes with many such methods.

data.frame()は、各引数をas.data.frame()という関数に渡します。この関数は総称関数なので、適切なメソッドを選んで引数をdata.frameに変換します*1

Character variables passed to data.frame are converted to factor columns unless protected by I or argument stringsAsFactors is false.

出ました。にっくきstringsAsFactorsです。しかし、前者の|()を使う方法は知りませんでした。こんな感じです。

d <- data.frame(
  x = c("A", "B"),
  y = I(c("A", "B"))
)

str(d)
#> 'data.frame':    2 obs. of  2 variables:
#>  $ x: Factor w/ 2 levels "A","B": 1 2
#>  $ y:Class 'AsIs'  chr [1:2] "A" "B"

どうも、data.frame()のこころは、

  • デフォルトだといい感じに変換するよ
  • 勝手に変換するのをやめてほしかったらI()を使ってね

ということのようです。ということはひょっとしてリストも...?と思って読み進めていくと、案の定です。

If a list or data frame or matrix is passed to data.frame it is as if each component or column had been passed as a separate argument (except for matrices protected by I).

こんな感じで、I()なしではlistを受け付けませんが、

data.frame(
  x = 1:2,
  y = list(list(1,2), list(1,2,3))
)
#>   x y.1 y.2 y.1.1 y.2.1 y.3
#> 1 1   1   2     1     2   3
#> 2 2   1   2     1     2   3

I()を使えばこの通りです。

data.frame(
  x = 1:2,
  y = I(list(list(1,2), list(1,2,3)))
)
#>   x       y
#> 1 1    1, 2
#> 2 2 1, 2, 3

書かれていませんが、ベクトルのリサイクルについても同じことが言えます。data.frame()は、長い方のベクトルの長さが短い方の定数倍であれば勝手にベクトルをリサイクルします。 しかし、I()をつけるとこれはエラーになります。

data.frame(x = 1:10, y = 1:2)
#>     x y
#> 1   1 1
#> 2   2 2
#> 3   3 1
#> 4   4 2
#> 5   5 1
#> 6   6 2
#> 7   7 1
#> 8   8 2
#> 9   9 1
#> 10 10 2

data.frame(x = 1:10, y = I(1:2))
#> Error in data.frame(x = 1:10, y = I(1:2)): arguments imply differing number of rows: 10, 2

もうお気づきかもしれませんが、この挙動はtibble()のデフォルトのものとほぼ同じです。 tibble()は、I()によって与えられた選択の自由ゆえに苦しんでいる人に差し伸べられた、「もうこっちがデフォルトでよくない?」という感じのこじんまりした救いの手なのです。たぶん。

「data.frame」とは?

しかし、そもそもdata.frameとはいったいなんなのでしょうか。R関連の英語を訳すときにはいつも悩むんですが、これには3つの意味があります。

  • data.frame(クラス)
  • data.frame()(コンストラクタ)
  • data frame(概念)

まず初めに思うのは、クラスとしての「data.frame」でしょう。しかし、S3のクラスなので、そんなものはいくらでもつくれます。 あの、変数名として使われがちだけどほんとうは関数なdfでさえ、data.frameになれます。

class(df) <- "data.frame"
df
#> NULL
#> <0 rows> (or 0-length row.names)

では、コンストラクタでしょうか。クラスを見てもそれが由緒正しいdata.frameとはわかりませんが、さすがにdata.frame()で生み出される結果はdata.frameだと言えるのではないでしょうか。 そうであるなら、関数としてのdata.frame()こそがdata.frameの本質です。

しかし、それも違う気がします。上で見たように、data.frame()は引数をどのように変換するかはas.data.frame()に委ねています。data.frameがあるべき姿を決めるのはdata.frame()ではないのです。

そんなわけで、「data.frame」は、我々の心の中にしかありません。 あの四角い感じの、みんなが「data.frame」と呼んでいるものこそが「data.frame」なのです。 その定義をクラスとしてのdata.frameや、コンストラクタとしてのdata.frame()になすりつけることはできません。

そもそも、「data.frame」という概念は今やRだけのものではありません。PandasやJulia、Sparkなどなど様々な言語に飛び火しています。 たとえRの言語仕様に「data.frameとはこうだ!」みたいな文言があったとしても、それに甘えてはいられないでしょう。 完璧なdata.frameは存在しませんが、自分たちで作っていくしかありません。

tibbleはそんな「おれがかんがえたさいきょうのdata.frame」のひとつです。 信じすぎず、ディスりすぎず、議論を見守っていきましょう。というのが私のスタンスです。

*1:めっちゃどうでもいいですが、behaviourってなってるのを見て、イギリス英語なのはHadleyだけじゃなくてRのコアからしてそうなのかーと実感しました