2020/03/21

R - 入門本編 6章 リストとデータフレーム

R の再帰的データ構造である「リスト」と、その特別な形である「データフレーム」について理解する。どちらも実践的に R を使っていくのに必須なデータ構造なので、その操作に習熟しておかなければならない。

リスト

「リスト」とは、順序付きのオブジェクトの集まりである。リスト内の個々のオブジェクトのことをコンポーネントと呼ぶ。

リストの生成

リストは list() を用いて生成する。
> Lst <- list("no tag name", stringcomp="ABC", numericcomp="2.71", vectorcomp=c(3, 1, 4), arraycomp=array(dim=c(2, 2, 2)));
> Lst
[[1]]
[1] "no tag name"

$stringcomp
[1] "ABC"

$numericcomp
[1] "2.71"

$vectorcomp
[1] 3 1 4

$arraycomp
, , 1


     [,1] [,2]
[1,]   NA   NA
[2,]   NA   NA

, , 2

     [,1] [,2]
[1,]   NA   NA
[2,]   NA   NA

コンポーネントのモードは同一である必要はなく、文字列や数値のスカラーも入るし、ベクトルも配列も入る。引数にタグ名が付いていれば、それがコンポーネントの名前になる。ベクトル同様、要素数は length() で得られる。

リストの正体

リストオブジェクトの正体を確認してみる。
> mode(Lst)
[1] "list"
> attributes(Lst)
$names
[1] ""            "stringcomp"  "numericcomp" "vectorcomp"  "arraycomp"

モードは list だ。これまで見てきたスカラー (要素数 1 のベクトル)、ベクトル、配列、行列はいずれもモードはアトミックなもの (numeric, character, logical, complex) を持っていたが、リストはモード自体が list である。コンポーネントの名前は names という属性に入っている。ちなみに attributes() の返す結果はリストだ。names という名前のコンポーネントを持ち、その中身は character の配列になっている。

リストの内容の参照

リストのコンポーネントには順序が付いているので、順序を示す添字を用いて参照できる。ベクトルでは添字 (添字ベクトル) の指定に「[]」を用いたが、リストでは添字の指定に「[[]]」を用いる。添字に指定できるのは、要素数が1のベクトルのみである。リスト内の各コンポーネント値のモードは同一とは限らないため、結果を1つのベクトルで返せないからだ。
> Lst[[1]]
[1] "no tag name"
> Lst[[6]]
 以下にエラー Lst[[6]] :  添え字が許される範囲外です
> Lst[[6]] <- TRUE
> Lst[[6]]
[1] TRUE

ベクトルと違い、要素数を越える添字を指定して参照しようとするとエラーとなる (ベクトルでは NA を返す)。代入は可能でリストの要素数が拡張される。

「[[]]」ではコンポーネントの名前でも参照ができる。
> Lst[["stringcomp"]]
[1] "ABC"
> Lst[["stringcom"]]
NULL

存在しない名前が指定された場合は NULL を返す (数値が指定されたときと異なり、エラーにはならない)。存在しない名前を指定しての代入も可能で、この場合リストの末尾にコンポーネントが追加されることになる。NULL はモード NULL の特殊な値で、値が存在しないことを意味している (おそらく文脈に応じたモードの「空っぽなもの」に自在に変化する値だと思われる)。

「[[]]」ではベクトルを指定できないが、「[]」を用いて添字ベクトルを指定することで、リストの一部分をリストとして取り出すことならできる。
> Lst[1]
[[1]]
[1] "no tag name"

> Lst[2:3]
$stringcomp
[1] "ABC"

$numericcomp
[1] "2.71"

「[[]]」はベクトルや名前付きベクトルに対しても同様に振る舞う。

コンポーネント名は「[[]]」のかわりに「$」を用いて指定することができる。
> Lst$numericcomp
[1] "2.71"
> Lst$n
[1] "2.71"

「$」では、一意に特定できれば短く省略しても良い。

より複雑なリストの生成・変更

list(name_1=object_1, ..., name_m=object_m) でm個のオブジェクトからなるリストが生成される。タグが無ければ名前のないコンポーネントになる。コンポーネントはコピーされ、オリジナルとは関連がなくなる。
要素の追加/置換は上で見たように Lst[[index]] や Lst[[name]] を用いる以外に、以下のような方法もある。
> Lst[8:9] <- list(1, 2)
> Lst[[8]]
[1] 1
> Lst[[9]]
[1] 2

左辺のリストの8〜9番目のコンポーネントとして、右辺のリストを代入する操作である。右辺のリストのコンポーネントに名前がついていても、それは失われる。
右辺はベクトルでも良い。以下のようにする。なお、名前付きベクトルでもやはり名前は無視される。
> Lst[8:9] <- c(3, 4)
> Lst[[8]]
[1] 3
> Lst[[9]]
[1] 4

c() はリストの結合にも用いることができる。
> list.A <- list(a=1, b=2, c=3)
> list.B <- list(a=4, b=5)
> list.AB <- c(list.A, list.B)
> list.AB
$a
[1] 1

$b
[1] 2

$c
[1] 3

$a
[1] 4

$b
[1] 5
> list.AB$a
[1] 1
> list.AB$b
[1] 2

結合元のリストのコンポーネント名も引き継がれていることが分かる。コンポーネント名に重複がある場合、名前で参照できるのは同名のコンポーネントのうち、添字の一番若い要素のようだ。

データフレーム

「データフレーム」は関係データベースのテーブルのようなデータ構造だ。クラス data.frame を持つリストであるが、ただのリストに比べてコンポーネントになり得るオブジェクトにいくつかの制限が設けられている。

  1. コンポーネントは、ベクトル、因子、数値行列、リスト (データフレームを含む) でなければならない
    • 例えば、3次元以上の配列や数値以外の配列・行列は NG になる
  2. 行列、リスト (データフレームを含む) は、それらが持つ列、要素 (およびデータフレームの変数) と同じ数の変数を新しいデータフレームに追加する
  3. 文字列ベクトルは因子に強制変換 (as.factor()) され、その他のベクトル、因子はそのままの状態で保持される
  4. データフレームの変数として現れるベクトルは全て同じ要素数、行列は同じ行数を持つ必要がある

これらの制限の結果として、データフレームは異なったモードや属性を持つ列の集合した、行列のようなものとみなすことができる。つまりデータフレームは、コンポーネント1つ1つが行列の列にあたり (そしてコンポーネント名が列名)、各コンポーネントの要素数 (これは上記の条件4により、統一されている) が行数であるような表形式のデータ構造になる。
行列形式で表示することもでき、行列の添字形式で参照することもできる。

データフレームの生成と操作

データフレームは、上記の要件を満たしていれば、data.frame(name_1=object_1, ...) を用いてリストと同様に生成できる。要件を満たすリストから as.data.frame(list) を用いて作ることもできる。
また、外部ファイルから read.table() を用いて作ることもできる。

データフレームに対して「$」を用いて列を参照するのは、毎回データフレーム名を指定しなければならないので、面倒、冗長である。R では操作したいデータフレームの列名 (に限らず、リストのコンポーネント名) を一時的に変数として参照することができるようにする機能 attach() がある。
例えば、以下のようなデータフレーム lentils を考える。
> lentils <- data.frame(u=c("A", "B", "C"), v=c(1, 2, 3), w=c(TRUE, FALSE, TRUE))
> lentils
  u v     w
1 A 1  TRUE
2 B 2 FALSE
3 C 3  TRUE

これに対して、lentils$u, lentils$v, lentils$w を u, v, w という変数で参照できるようにするのが attach(lentils) だ。
> attach(lentils)
> u
[1] A B C
Levels: A B C
> v
[1] 1 2 3
> w
[1]  TRUE FALSE  TRUE

attach(lentils) とすることによって、このデータフレームの変数 u, v, w が識別子の探索リストの2番目の置かれる。デフォルトの名前空間 (.GlobalEnv) は探索リストの1番目なので、u, v, w といった識別子が定義済でない限り、u, v, w が lentils の変数を意味するようになる。
例えば attach() 前に v が定義済だったとすると、以下のようになる。
> v <- 1
> attach(lentils)
The following object(s) are masked _by_ '.GlobalEnv':

    v
> v
[1] 1

なお、この状況で新たに u に付値するなどしても、u が探索リストの1番目に新たに置かれることになり、lentils$u を変更することにはならない。
> u <- v + w
> u
[1] 2 1 2
> lentils
  u v     w
1 A 1  TRUE
2 B 2 FALSE
3 C 3  TRUE

lentils$u を変更したい場合は、当然のことながら lentils$u と指定すればよい。
> lentils$u <- v+w
> lentils
  u v     w
1 2 1  TRUE
2 2 2 FALSE
3 4 3  TRUE

データフレームを探索リストから外すには、detach() を行う。detach() は探索リストの2番目にあるものを外す (多重に attach() したときは、最後に attach() したものにあたる)。detach(lentils) とか detach("lentils") といった風に名前を指定した方が間違いはないだろう。detach() しすぎると、読み込み済のパッケージの名前空間が探索リストから外されてそのパッケージの機能が使えなくなることがあるので気を付けよう。
現在の探索リストの状況は search() で見ることができる。
> search()
[1] ".GlobalEnv"        "package:stats"     "package:graphics"  "package:grDevices" "package:utils"     "package:datasets"
[7] "package:methods"   "Autoloads"         "package:base"  
> attach(lentils)
> search()
 [1] ".GlobalEnv"        "lentils"           "package:stats"     "package:graphics"  "package:grDevices" "package:utils"  
 [7] "package:datasets"  "package:methods"   "Autoloads"         "package:base"  
> detach(lentils)
> search()
[1] ".GlobalEnv"        "package:stats"     "package:graphics"  "package:grDevices" "package:utils"     "package:datasets"
[7] "package:methods"   "Autoloads"         "package:base"  

データフレームを使った問題解決は以下のようにやると良い。

  • 適切に定義、分離された問題に対して、その全ての変量に適切な名前を付けて問題ごとに1つのデータフレームに集積する
  • 個々の問題を処理する際は、適切なデータを attach() し、デフォルトの名前空間を作業用に使う
  • 問題の処理を終える前に、保存しておきたいデータはデータフレームに追加しておき、それから detach() する
  • 終わったら不要な変量は全て rm()