檜山正幸のキマイラ飼育記 このページをアンテナに追加 RSSフィード Twitter

キマイラ・サイトは http://www.chimaira.org/です。
トラックバック/コメントは日付を気にせずにどうぞ。
連絡は hiyama{at}chimaira{dot}org へ。
蒸し返し歓迎!
このブログの更新は、Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama
ところで、アーカイブってけっこう便利ですよ。

2015-12-15 (火)

R言語のファクターの異常な分かり難さは、冗談か嫌がらせか

| 09:45 | R言語のファクターの異常な分かり難さは、冗談か嫌がらせかを含むブックマーク

R言語ファクターというデータ型(みたいなもの)があるんですが、これが分かりにくい。一応納得した後でも「なんで、こんなことになったんだろう?」「たちの悪い冗談だったのか?」という疑念が拭えません。

ご説明いたしましょう。

内容:

  1. どのくらい奇妙か
  2. そもそもファクターって何?
  3. 錯綜してしまったファクターの用語法
  4. ファクター・データ生成の詳細
  5. ややこしいデフォルト処理

どのくらい奇妙か

ファクター(factor, 因子)とは、Rで扱うデータの一種です。factor()関数がそのコンストラクタです。factor()関数で、ファクター・データを作ってみます。

  • 元にするベクトル・データは c(5, 0, 5, 0, 10) です。
  • levels(レベル達)という名前の引数に c(10, 5, 0) を指定します。
  • labels(ラベル達)という名前の引数に c("a", "b", "c") を指定します。

これを実行して、できたデータxを調べてみます。

> x <- factor(c(5, 0, 5, 0, 10), levels=c(10, 5, 0), labels=c("a", "b", "c"))
> levels(x)
[1] "a" "b" "c"
> labels(x)
[1] "1" "2" "3" "4" "5"
> as.integer(x)
[1] 2 3 2 3 1
> 

  • levelsを c(10, 5, 0) と指定したデータのlevelsは c("a", "b", "c") でした。
  • labelsを c("a", "b", "c") と指定したデータのlabelsは c("1", "2", "3", "4", "5") でした。
  • c(5, 0, 5, 0, 10) から作ったファクター・データを整数値ベクトルに変換すると、c(2, 3, 2, 3, 1) でした。

常人が理解できる状況ではありません。これはいったい、何が起こっているのでしょうか?

そもそもファクターって何?

R言語のファクターは、他の言語の列挙型(enum型)と似たものです。いやっ、あんまり似てないか? まー、用途は列挙型と共通しているとは言えます。

例えば、ある程度の人数の社員データがあり、性別がgenderという名前のベクトルで記述されるとしましょう。男性を"MALE"、女性を"FEMALE"という文字列で表す約束をすると、genderベクトルは、"MALE", "MALE", "FEMALE", ... というような長い列になります。

文字列"MALE"、"FEMALE"は人間が読むには適してますが、メモリを消費するし*1扱いにくいこともあります。こんな状況で列挙型(enum型)の値が使われます。次は、TypeScriptによる列挙型の定義です。

// 男女の別
enum Gender {
    MALE,   // 男性 = 0
    FEMALE, // 女性 = 1 
    UNKNOWN // 不明 = 2
}

プログラム中ではMALE、FEMALE、そしてUNKNOWNという名前を使えますが、実際には整数値なのです。コメントに書いてある 0, 1, 2 が名前に対応する整数値です。ここでは、値に付けられた名前を表示名、実際の整数値を内部値と呼ぶことにします。

表示名に対する内部値を、明示的に指定することもできます。

// 男女の別
enum Gender {
    MALE    = -1,
    FEMALE  = 1,
    UNKNOWN = 0
}

この例では、表示名と内部値の対応は次のようになります。

表示名 内部値
MALE -1
FEMALE 1
UNKNOWN 0

このような対応を、表示名-内部値・マッピングと呼ぶことにします。列挙型を定義するとは、表示名-内部値・マッピングを指定することに他なりません。

R言語のファクターは、型ではないのですが、*2個々のベクトルに表示名-内部値・マッピングを持たせることによって、列挙型データと同様な扱いを可能としたものです。

錯綜してしまったファクターの用語法

R言語のファクターの場合、水準という言葉が使われます。困ったことに、「水準」が2つの意味で使われて解釈が難しいので、ここでは水準ラベル水準値と使い分けます。その意味は、列挙型と対応させて理解してください。(列挙値とは、列挙型のインスタンスのことです。)

多くのプログラミング言語 R言語
列挙値を要素とする配列 ファクター・データ
列挙値の表示名 水準ラベル
列挙値の内部値 水準値

「水準」が、水準ラベルと水準値のどちらを意味するか曖昧だったり、さらには、「水準値」と言っても実は水準ラベルの意味だったりして、頭が痛くなります

ここで、先に挙げたファクター・データ生成のコマンド(関数呼び出しと代入)をもう一度見てみましょう。

> x <- factor(c(5, 0, 5, 0, 10), levels=c(10, 5, 0), labels=c("a", "b", "c"))

factor()関数の最初の引数は必須で、ファクター・データを作るための元データを指定します。与えられた元データに対してファクター・データを作るには、次の2段階の処理が必要です。

  1. 元データの値を水準値(内部値)に変換する。
  2. 結果のベクトルに、水準ラベル(表示名)の情報をくっ付ける。

詳しくは次節で述べますが、水準値への変換は、c(5, 0, 5, 0, 10) → c(2, 3, 2, 3, 1) となります。水準ラベルの情報は、(labelsではなく)levelsという属性としてくっ付けます。

> as.integer(x)
[1] 2 3 2 3 1
> attr(x, "levels")
[1] "a" "b" "c"
> 

levels、labelsという名前がどのように使われているかをまとめます。驚くべきヒドサです。

元データ値→水準値変換を指定する引数名 levels 値は元データと同じ型
水準ラベルを指定する引数名 labels 値はラベル
水準ラベル情報を持つ属性名 levels 値はラベル
水準ラベル情報を取得する関数名 levels
ファクターと何の関係もない関数名 labels 騙されるな!

ファクター・データ生成の詳細

ファクターに関する用語・名前は錯綜していて紛らわしいのですが、それだけでなく、ファクター・データ生成の手順はけっこう複雑なのです。こりゃ分かりにくいわ。

まず注意すべきは、R言語のファクターの場合、水準値=内部値は自動的に決まり、ユーザーは変更できないことです。常に、1から始まる連番整数値が水準値=内部値として使われます。内部値なので、Rは実際の値を隠そうとします。代わりに、水準ラベル=表示名を見せるようにします。

> x <- factor(c(5, 0, 5, 0, 10), levels=c(10, 5, 0), labels=c("a", "b", "c"))
> x
[1] b c b c a
Levels: a b c
> 

ファクター・データ生成の過程では、2つのマッピング(対応表)が作られます。“元データ値-水準値・マッピング”と“水準ラベル-水準値・マッピング”です。対応は1:1なので、どちらのマッピングも可逆、つまり対応方向を逆にして使うことができます。

factor()関数のlevels引数で“元データ値-水準値・マッピング”を指定し、labels引数では“水準ラベル-水準値・マッピング”を指定します。

levels=c(10, 5, 0) の解釈は:

元データ値 水準値
10 1
5 2
0 3

このマッピングに従い、元データをファクター・データ(実体は整数ベクトル)に変換します。実例では、c(5, 0, 5, 0, 10) → c(2, 3, 2, 3, 1) でしたね。

labels=c("a", "b", "c") の解釈は:

水準ラベル 水準値
"a" 1
"b" 2
"c" 3

このマッピングにより、表示名と内部値の相互変換をします。labels引数に指定された文字列ベクトルは、ファクター・データのlevels属性としてそのままくっ付けます(labels引数がlevels属性になるぞ、注意、注意、注意!!)。

でき上がったファクター・データxの実体である整数ベクトルを見るには、as.integer(x) とします。xの水準ラベル=表示名を見るには、levels(x) です、labels(x) ではないのですよ。labels()関数は、ファクターの水準ラベル=表示名とは関係ありません

ややこしいデフォルト処理

factor()関数のlevels引数が指定されなかった場合、“元データ値-水準値・マッピング”は次のようにして作られます。

  • 元データに出現した値のソート順に、水準値 1, 2, ... を割り振る。

元データが c(5, 0, 5, 0, 10) の場合は次のようなマッピングになります。

元データ値 水準値
0 1
5 2
10 3

> x <- factor(c(5, 0, 5, 0, 10), labels=c("a", "b", "c"))
> as.integer(x)
[1] 2 1 2 1 3
> levels(x)
[1] "a" "b" "c"
> x
[1] b a b a c
Levels: a b c
> 

labels引数が指定されなかった場合、“水準ラベル-水準値・マッピング”は次のようにして作られます。

  • “元データ値-水準値・マッピング”の元データを文字列化する。

> x <- factor(c(5, 0, 5, 0, 10), levels=c(10, 5, 0))
> as.integer(x)
[1] 2 3 2 3 1
> levels(x)
[1] "10" "5"  "0"
> x
[1] 5  0  5  0  10
Levels: 10 5 0
> 

このケースでは:

元データ値 水準値
10 1
5 2
0 3

水準ラベル 水準値
"10" 1
"5" 2
"0" 3

levels引数もlabels引数も省略されると、デフォルトのlevels(元データ値-水準値・マッピング)を作り、それを元にしてlabels(水準ラベル-水準値・マッピング)を作ります。くどい注意ですが、labels情報はlevels属性になります。

> x <- factor(c(5, 0, 5, 0, 10))
> as.integer(x)
[1] 2 1 2 1 3
> levels(x)
[1] "0"  "5"  "10"
> x
[1] 5  0  5  0  10
Levels: 0 5 10
> 

この場合の結果であるxは、一見すると元データと変わってません。しかし、実体は水準値=内部値のベクトルに変換され、水準ラベル=表示名の文字列ベクトルが値であるlevels属性が付与されています。


レベルとラベルって、そもそも似ていて紛らわしい言葉なのに、この混乱した使用法、いいかげんにしろよと言いたい。

*1:文字列の代わりにシンボルを使えばメモリの節約になります。文字列であっても、変更されるまでは領域を共有する方式ならメモリを節約できます。

*2[追記]"factor"というclass属性が付いているので、Rのクラスを型と呼んでいいなら、ファクター型は存在します。その意味で「型ではない」は言い過ぎなので取り消します。[/追記]

トラックバック - http://d.hatena.ne.jp/m-hiyama/20151215/1450140312