因子とは実際のところ何なのか
因子は、カテゴリデータ — 治療群、アンケートの回答、T シャツのサイズのように、決まった選択肢の中から取られる値 — に対する R の答えです。表示すると文字列ベクトルのように見えますが、そうではありません。内部的には因子は 整数コードのベクトルと、水準(levels)と呼ばれるラベルの参照表 の組み合わせです。
出力にはラベルが表示され、続いて選択肢を並べた Levels: の行が出ます。as.integer() はその仕組みを露わにします。コードは 3 1 3 2 として返りますが、これは各値が実際には水準表へのインデックスだからです。そして水準は既定でデータが到着した順ではなく アルファベット順 に並びます(large, medium, small)。したがって small はコード 3、large はコード 1 となり、直感とは一致しません。このアルファベット順の既定値を覚えておいてください。以下で扱う 2 つの罠の原因になります。
なぜ単純な文字列ではなく、この二層構造をわざわざ使うのでしょうか。統計にそれが必要だからです。モデルは "small" に係数を掛けることはできませんが、既知の 3 つの水準を 0 と 1 の列にダミーコード化することはできます。lm()、glm()、table()、そして ANOVA の裏側にある仕組みはいずれも、変数がカテゴリであること、カテゴリの完全な集合が何であるか(データに現れないものも含めて)、どのカテゴリがベースラインかを知るために因子を手がかりにします。素の文字列ベクトルはそのいずれも持ちません。
因子の作成: levels、labels と table()
既定では factor() は見つかった一意な値を取り、アルファベット順に並べて水準にします。多くの場合、水準の 集合 と 順序 の両方を制御したくなるので、levels = を渡します。
これで水準は自然なサイズ順に並び、因子とともに常用することになる 1 行の度数集計 table() も、その順序で件数を報告します。nlevels() はカテゴリ数を数えます。
levels = はさらに 2 つの利点をもたらします。指定した水準リストに含まれないデータ中の値は NA になります(これは良いことです。誤記が独自のカテゴリになる代わりに表面化します)。また、出現数がゼロの水準も存在し続けるため、アンケート回答の要約に「強く反対: 0」と表示され、その選択肢が最初から無かったかのように振る舞うことはありません。
labels = は作成時に水準の名前を付け替えます。生データがコードを使っている場合に便利です。
既定の挙動で問題ない場合、既存のベクトルを手早く変換するには as.factor(x) を使います。
順序データのための順序付き因子
素の因子はカテゴリを順序なしとして扱います — "red" は "blue" より小さいわけではありません。しかし、カテゴリ尺度の中には本物の順位を持つものがあります。low/medium/high、反対/中立/賛成など。それは ordered = TRUE で宣言します。
出力には Levels: low < medium < high と表示され、比較演算子が機能します。ある評価が別の評価を上回るかどうかを尋ねたり、"medium" 以上のすべてに絞り込んだりできます。順序なしの因子ではどちらもエラー(正確には警告と NA)になります。順序付き因子はモデルが変数をコード化する方法も変え(ダミー変数ではなく多項式コントラスト)、順序尺度の予測変数では通常これが望ましい挙動です。
ordered = TRUE は順位が本物である場合にのみ使ってください。通常のグループを順序付きとして符号化すると、モデル出力が誤読しやすい形に変わってしまいます。
基準水準と relevel()
因子の 最初の 水準は特別です。モデル関数はそれを 基準カテゴリ、つまり他のすべての水準の係数が測られるベースラインとして扱います。既定の水準順はアルファベット順なので、介入しない限りベースラインはアルファベットによって選ばれます — "control" がアルファベット順で "aspirin" に負けるのは、科学的な判断ではありません。
relevel() は水準を先頭に昇格させます。
変更前は、control がたまたまアルファベット順の運で先頭にあるだけです。relevel(..., ref = "control") の後は 意図して 先頭になっています。この予測変数を使った 線形回帰 では、treatment の係数が「treatment は control とどう違うか?」という、実際に問いたかった質問に答えるようになります。モデルのカテゴリ係数が分かりにくく見えるときは、まず基準水準を確認してください。
(先頭だけでなく完全に並べ替えたい場合は、factor() に完全な levels = ベクトルを再度渡します。)
典型的な罠: 因子を数値に変換する
数値が因子として届くことがあります。典型的には、数値でない値が紛れ込んだ CSV の列です。元に戻す変換は一見自明ですが、記憶に残る形で失敗します。
as.numeric(f) は 2 1 3 を返します。20, 10, 30 ではなく、水準コード です。水準はアルファベット順に "10", "20", "30" と並ぶため、"20" は水準 2 であり、変換結果は… 2 になります。エラーも警告もなく、もっともらしい小さな整数が静かにデータを置き換えます。これが原因で撤回された分析すらあります。
正しい経路は文字列を経由します。as.numeric(as.character(f)) はまずラベルをテキストとして復元し、次にそのテキストを数値として解析して 20 10 30 を得ます。この定型句を焼き付けてください。因子から数値への変換は常に as.character() を経由する。
droplevels() と stringsAsFactors の物語
因子を部分抽出しても、もはや現れないカテゴリを含めて水準の集合はそのまま残ります。
large を除外した後も、table() はそれを報告します — 件数 0 として。それがまさに正しい場合もあります(空のカテゴリを見えるようにしておきたい場合)。そうでない場合 — 件数 0 のグループはプロットを散らかし、層別解析を壊すこともあります — droplevels() が観測のない水準を破棄します。
古いコードや Stack Overflow の回答を読むときに必要になる歴史的な注記があります。R 4.0(2020 年)以前は、data.frame() と read.csv() がすべての文字列列を自動的に因子に変換していました — stringsAsFactors = TRUE が既定だったのです。10 年分のチュートリアルには、誰も望んでいない因子への回避策が散らばっています。R 4.0 以降は既定が FALSE になりました。文字列は文字列のまま残り、データフレーム 内の変数が本当にカテゴリである場所で、意図的に因子を作成します。それが正しい習慣です — 明示的に、意図をもって、自分で選んだ水準とともに因子を作りましょう。
この記事のまとめ
- 因子 = 整数コード + 水準ラベル。統計関数に「この変数はカテゴリだ」と伝えるものです。
levels =でカテゴリの集合と順序を制御し、labels =で名前を付け替え、table()で数えます。ordered = TRUEは本当に順序尺度である場合に比較を可能にします。- 最初の水準がモデルのベースラインです。
relevel(f, ref = ...)で意図的に設定しましょう。 as.numeric(f)を直接使ってはいけません — 常にas.numeric(as.character(f))を使います。- 部分抽出の後は
droplevels()で未使用の水準を消去します。R 4.0 以降、自分で因子を作らない限り文字列は文字列のままです。
次は データフレーム — 因子、数値、テキストが 1 つの表の列として同居する場所です。
よくある質問
R の因子(factor)とは何ですか?
因子は R におけるカテゴリデータ用の型で、水準(levels)と呼ばれる決まった選択肢の集合から取られた値を表します。内部的には整数コードのベクトルと水準ラベルの表から成り、この仕組みによって統計関数はカテゴリを自由記述のテキストとしてではなく、カテゴリとして正しく扱えます(集計したり、モデル内でダミーコード化したり)。
R で因子を数値に変換するにはどうすればよいですか?
文字列を経由します: as.numeric(as.character(f))。as.numeric(f) を直接呼ぶと、ラベルが示す値ではなく内部の水準コード(1, 2, 3, ...)が返るため、"20" と表示される因子が 2 になってしまいます。これは R で最もよくある、静かに起こるバグの 1 つです。
R の relevel() は何をしますか?
選んだ水準を先頭の位置へ移動します: relevel(group, ref = "control")。先頭の水準は基準(ベースライン)カテゴリであり、lm() や glm() などのモデル関数が他のすべての水準を比較する対象になります。意図して選ぶことで、回帰係数が狙いどおりの意味を持つようになります。
削除したはずの水準がまだ因子に表示されるのはなぜですか?
因子を部分抽出しても、もはや出現しない水準を含めて水準の集合はそのまま保持されます。そのため table() は度数 0 のカテゴリを表示し、モデルもその分の領域を確保します。部分集合に対して droplevels() を実行すると、使われていない水準が破棄されます。