統計コンサルの議事メモ

統計や機械学習の話題を中心に、思うがままに

randomForestではCharacterは使わないようにしよう

RのrandomForestを使っていてはまったのでメモしておきます。

①目的変数がcharacterだと分類として扱ってくれない

最初にはまったのがこちらでした。目的変数がcharacterだとカテゴリ変数として扱ってもらえないため、分類ではなく回帰としてプログラムが進んでしまい、エラーが返ります。

まずは以下のようにデータを読み込みます。ちなみにこのデータはTelco Customer Churnで、 KaggleからDLしてきました。

library(tidyverse)
library(randomForest)
d <- read_csv("./Data/WA_Fn-UseC_-Telco-Customer-Churn.csv")

このデータを使って下記のようにrandomForestを実行します:

> randomForest(Churn ~ gender, d)
 y - ymean でエラー:  二項演算子の引数が数値ではありません 

「何このエラー??」と思いながらrandomForestの中身を見てみると、以下のような記述が見つかります。

addclass <- is.null(y)
classRF <- addclass || is.factor(y)

classRFというのはカテゴリ変数であるかを判定するbooleanなのですが、is.factorで判断しているんですね。ここでFALSEが返ると、後の工程で分類のためのプログラム(Cで書かれたもの、多分コレのL38 ~ L540)ではなく、回帰用のプログラム(多分コレのL22 ~ L340)が呼ばれてしまうようです。

ちなみにrandomForestはformulaでもmatrixでも対応できるような総称関数になっているので、 コンソールでrandomForestと叩いても中身を見ることはできません。そのような場合、まずはmethodsでどのような関数が含まれているかを確認しましょう。

> methods(randomForest)
[1] randomForest.default* randomForest.formula*
see '?methods' for accessing help and source code

randomForestという関数はrandomForest.defaultrandomForest.formulaという関数を総称しているようです。前者がメインのようなので以下のコマンドを実行します。

getS3method("randomForest", "default")

そうすると先程の関数定義を確認することができます。さらにちなみに、lookupというパッケージを用いることでそういった総称関数やCで書かれた関数などをRStudio上でシンタックスハイライトさせながら表示することができます。大変便利ですので、是非こちらの記事を参考にして使ってみてください。

②説明変数がcharacterだとダミー化してくれない

次にはまったのがこちらでした。さきほどエラーが返ってきたrandomForestを、目的変数をfactorに直して実行してみましょう:

d2 <- 
   d %>% 
   mutate(Churn = as.factor(Churn))
> randomForest(Churn ~ gender, d2)
 強制変換により NA が生成されました  randomForest.default(m, y, ...) でエラー: 
   外部関数の呼び出し (引数 1) 中に NA/NaN/Inf があります

NAがありますというエラーなのですが、このデータではNATotalCharges列にしかありませんので、どうやら途中で生成されているようです。ちなみにRStudioでRMarkdownを使っているとき、チャンク内で実行すると上記のエラーが表示されるのですが、コンソールで実行すると以下のようにもう少し情報が追加されます:

> randomForest(Churn ~ gender, d2)
 randomForest.default(m, y, ...) でエラー: 
   外部関数の呼び出し (引数 1) 中に NA/NaN/Inf があります 
 追加情報:  警告メッセージ: 
 data.matrix(x):   強制変換により NA が生成されました 

data.matrixが悪さしているようですね。私は普段チャンク内で実行させることが多いため、 この表示に気付かなくて時間を無駄にしました。

randomForest.defaultを見ると以下の記述があります。

if (is.data.frame(x)) {
   xlevels <- lapply(x, mylevels)
   ncat <- sapply(xlevels, length)
   ncat <- ifelse(sapply(x, is.ordered), 1, ncat)
   x <- data.matrix(x)

ここでxをdata.matrix(x)でダミー化しようとしたものの、characterであったために失敗しているようですね。

> d2 %>% select(Churn, gender) %>% str()
Classes ‘tbl_df’, ‘tbl’ and 'data.frame':  7043 obs. of  2 variables:
 $ Churn : Factor w/ 2 levels "No","Yes": 1 1 2 1 2 2 1 1 2 1 ...
 $ gender: chr  "Female" "Male" "Male" "Male" ...
> d2 %>% select(Churn, gender) %>% data.matrix(.) %>% head()
 強制変換により NA が生成されました      Churn gender
[1,]     1     NA
[2,]     1     NA
[3,]     2     NA
[4,]     1     NA
[5,]     2     NA
[6,]     2     NA

それではfactorに直して実行してみましょう。

d3 <- d2 %>% select(Churn, gender) %>% mutate(gender = as.factor(.$gender))
> str(d3)
Classes ‘tbl_df’, ‘tbl’ and 'data.frame':  7043 obs. of  2 variables:
 $ Churn : Factor w/ 2 levels "No","Yes": 1 1 2 1 2 2 1 1 2 1 ...
 $ gender: Factor w/ 2 levels "Female","Male": 1 2 2 2 1 1 2 1 1 2 ...
> randomForest(Churn ~ gender, d3)

Call:
 randomForest(formula = Churn ~ gender, data = d3) 
               Type of random forest: classification
                     Number of trees: 500
No. of variables tried at each split: 1

        OOB estimate of  error rate: 26.54%
Confusion matrix:
      No Yes class.error
No  5174   0           0
Yes 1869   0           1

ようやく動くようになりました。

このようなエラーにはまらないためには、例えばread_csvの代わりに(StringsAsFactorsFALSEにしないで)read.csvを使うか、randomForestの代わりにrangerを使うという手があります。

> ranger::ranger(Churn ~ gender, d)
Ranger result

Call:
 ranger::ranger(Churn ~ gender, d) 

Type:                             Classification 
Number of trees:                  500 
Sample size:                      7043 
Number of independent variables:  1 
Mtry:                             1 
Target node size:                 1 
Variable importance mode:         none 
Splitrule:                        gini 
OOB prediction error:             26.54 % 

もとのデータでも動いていますね!

終わりに

今回のエラーはモデリングに入る前にダミー化を行っていれば防げたものでしたが、Rだとカテゴリ変数をそのまま渡しても動くものが多いのでついサボってしまいました。普段からダミー化をする習慣が身についている人や、多分Pythonでははまらないんでしょうね。

ただ今回はエラーを追いかけながら総称関数のソースの便利な確認方法を知ることができたので良かったです。