このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

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

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

ご説明いたしましょう。

内容:

どのくらい奇妙か

ファクター(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のクラスを型と呼んでいいなら、ファクター型は存在します。その意味で「型ではない」は言い過ぎなので取り消します。[/追記]