[R] 予測モデルを作るには formula を活用せよ
概要
formula
オブジェクトは変数変換や交互作用項など, 多彩な表現ができる.xgboost
やglmnet
ではmodel.matrix()
を併用することでformula
を利用できる.
統計モデリング/機械学習で予測モデルを構築するとき, 予測性能の向上のため, しばしば変数を入れ替えたり, 変換したりといった推敲が必要となる. R ではこういうときに formula
オブジェクトを使うと, いちいちデータフレームに変換後の数値を追加したり書き換えたりする必要がなくなる. glmnet
や xgboost
など, formula
が直接使えないものでも model.matrix()
等を併用すれば可能である*1.
formula
オブジェクトを解説した記事を探すと, かなり前から存在する. 例えば以下の記事.
m884.hateblo.jp
なお, 上記はタイトルが「formula とは? (1)」となっているが, 続編は確認できなかった.
もうひとつ有用な日本語のページがある. 以下では formula
で使える表現の一覧が紹介されている.
http://cse.naro.affrc.go.jp/takezawa/r-tips/r/71.html
今回は, これらを記事を踏まえて formula
のもう少し応用的な使い方を紹介する.
ちなみに, 以前に glmnet
の使い方について関連のありそうなことを ill-identified.hatenablog.com で書いた. ここでもこれから紹介する model.matrix()
を利用している.
formula オブジェクト
formula
構文はモデルの構造を記述に便利だが,
y ~ x1 + x2
というように最も簡単な回帰式にしか使っていない人も多いと思う. しかし, この formula
にはいろいろな構文がある. 例えば,
y ~ .
ならば入力データの全変数を説明変数とする.
y ~ . - x1
ならば, 全変数から x1
を除いたものを説明変数とする.
なお, formula
はデフォルトで定数項 (intercept) を含んでいる. これは-1
または+0
で除去する. 例えば全ての変数を使うが, 定数項は使いたくないならこうなる.
y ~ . -1
単純な追加・除去よりも複雑な操作もできる.
y ~ x1 * x2
ならば, x1
と x2
と, 2変数の交互作用項が追加される*2*3. また, log()
, sqrt()
, sin()
などの関数も利用できる. つまり,
log(y) ~ x1 + x2
ならば片対数モデルになる. log(x+1) ~ x1 + x2
も可能. さらに, I()
または poly()
で多項式回帰をすることも可能である. dplyr::lag()
を使えば自己回帰項を追加できる*4. また, 従来は lm()
のオプション引数で指定することが多いオフセット項も, offset()
を使うことで formula
の中で記述できる.
これらの演算子は, ()
で囲むことで優先順位を変えることもできるため, かなり複雑な変数の組み合わせも可能である*5. 例えば, 以上の構文の組み合わせ技として,
log(y) ~(. + poly(x1, 3)) * z1 - z1:x1
という式を書いた場合, 従属変数は y の対数となる. 説明変数は, データフレームの全変数と x1
の3次多項式に加え, これらと z1
の交互作用項を加え, そこから z1
と x1
の交互作用項だけ除いたものになる.
以上から, formula
をうまく活用できれば, 元のデータフレームに変数 x1
を2乗しただけ, 対数変換しただけ, といった列を大量に作成する必要がなくなる.
ここでは動作確認用サンプルとして, 以下のようなプログラムでデータフレームを作成する.
set.seed(42) n <- 30 df.train <- data.frame(y=rnorm(n)^2, x1=rnorm(n), x2=rnorm(n), z1=as.factor(sample(c("A", "B", "C"), size = n, replace = T, prob = c(.4, .4, .2)) ), z2=as.factor(sample(c("A", "B", "C"), size = n, replace = T, prob = c(.2, .2, .6)) ) ) df.test <- df.train[floor(n/2):n, ] df.test$y <- NULL df.train <- df.train[1:(floor(n/2)-1), ]
このデータフレームは x1
, x2
は numeric
で, z1
, z2
は 3種類の値をとる factor
である. 予測モデルを作るという前提なので, 訓練データ df.train
とテストデータ df.test
を用意している. df.test
には従属変数 y
は存在しない. 以降はこの2つのデータフレームがあるという前提で話をすすめる.
formula
オブジェクトは線形回帰モデルを推定する lm()
や, 一般化線形回帰モデルを推定する glm()
など, 多くの関数で使用できる. まずは片対数モデル の回帰をしてみる. わざわざ新しくyの対数を作成する必要はなく, 先ほど紹介したように
log(y)
を使って,
(m1 <- lm(formula=log(y)~x1+x2, data=df.train))
とする. さらにテストデータにモデルの予測値を与えたい場合は, 通常通り predict()
を使い,
predict(m1, df.test)
とする.*6
あるいは のような多項式回帰なら,
(m2 <- lm(formula=y~poly(x1, 3), data=df.train))
となる. これもやはり predict()
が利用可能である. これらは lm
オブジェクトが formula
オブジェクトの情報を保持しているので可能なことである.
formula が使えない場合
しかし, 一部のパッケージでは formula
オブジェクトに対応していないものがある. 具体的には glmnet
と xgboost
である. これらは formula 関数ではなく, matrix
オブジェクトしか受け付けない*7. このような場合は model.matrix()
を使う. この関数は データフレームを formula
の式に沿って matrix
に変換してくれる. ところで, かりに特殊な変数変換を行わない場合でも, factor
型や logical
型変数を含むデータを使う場合は注意が必要であることを言っておこう. 計算をおこなううえで, factor
/logical
型はゼロ-1のバイナリ変数をカテゴリの数だけ作成することが必要になるからである. 以下イメージ.
データフレームが
z1 (factor) | flag (logical) |
---|---|
A | TRUE |
B | TRUE |
C | FALSE |
となっていた場合, 以下のように変換することになる.
z1A | z1B | z1C | flagTRUE | flagFALSE |
---|---|---|---|---|
1 | 0 | 0 | 1 | 0 |
0 | 1 | 0 | 1 | 0 |
0 | 0 | 1 | 0 | 1 |
この変換後の数値化された行列はデザイン行列または計画行列と呼ばれる. lm()
等の関数は処理の中にこのデザイン行列への変換処理が組み込まれているので書く必要がなかったのだ.
model.matrix()
はformula
の変数変換だけでなく, このようなカテゴリ変数のデザイン行列への変換処理もおこなってくれる. たとえば,
model.matrix(log(y)~x1+x2+z1*z2, df.train)
のようにすれば, 式の右辺, つまり ,
に加え,
,
とその交差項のカテゴリ別のバイナリ変数を含むデザイン行列ができる.
model.matrix()
を使えばこのようなデザイン行列を作成するのが楽になるが, 従属変数の変換はしてくれない. どうせならば formula
を与えればすべて自動でやってくれるようにすれば楽なので, xgboost
を例にして, 以下のように formula
とデータフレームから xgb.Dmatrix
オブジェクトを作成する関数を定義する.
require(xgboost) ver. 0.6-4 # 片対数モデルを定義 model <- log(y)~x1+x2+z1*z2 # 変換用の関数定義 xgb.DMatrix2 <- function(formula, data){ xgb.DMatrix(data = model.matrix(formula, data), label = model.response(model.frame(formula, data))) } # xgboost で訓練 train <- xgb.train(data = xgb.DMatrix2(model, df.train), params = list(objective="reg:linear"), nrounds=10) # 訓練データに対する予測 predict(train, newdata=xgb.DMatrix2(model, df.train))
ここでは, model.response()
を使って formula
の左辺で評価した値をラベル値に与えている. この model.response()
は model.frame
オブジェクト*8のみを引数に取るため, model.frame()
関数を挟んでいる.
さらにテストデータに対して予測値を与える場合は, テストデータのデザイン行列が必要になるが, テストデータには従属変数がふくまれないためエラーとなる. この場合は以下のようにしてデザイン行列を作成できる.
model.matrix(model[-2], df.test)
formula の第2要素に従属変数の情報があるため, このように書けば従属変数の情報だけを除くことができる. 別解として,
model.matrix(delete.response(terms(model)), df.test)
とすることもできる. この話から分かるように, 先に定義した xgb.DMatrix2()
にテストデータを与えるとエラーが発生するので, より便利にするため以下のように改良する.
# 変換用の関数定義 xgb.DMatrix2 <- function(formula, data){ if(prod(all.vars(formula[-3]) %in% colnames(data)) == 1){ # data contains dep. variables return(xgb.DMatrix(data = model.matrix(formula, data), label = model.response(model.frame(formula, data))) ) } else{ # data does not contain dep. variables return(xgb.DMatrix(data=model.matrix(formula[-2], data))) } } # テストデータで予測 predict(train, newdata=xgb.DMatrix2(model, df.test))
これで, 従属変数を含むデータを与えた場合は訓練データとみなし, そうでない場合はテストデータとみなして処理を場合分けするようになった*9. ただし, このやり方には2点, 注意すべきことがある. 1つは, テストデータに y
という名前の変数があるものの, 値がすべて NA
であるようなデータを与えてしまった場合である. このとき, 欠損値が含まれている行は除外されてしまうので, 上記の関数では0行のマトリクスを返してしまうから, y
そのものを消す必要がある. もう1つは, この方法では予測値を従属変数の変換後の値で返してしまう, ということである. 今回の例では model
に片対数モデルが定義されているから, predict()
で返されるのは y
の予測値ではなく対数の予測値であり, 正しくは exp(predict(...))
と書く必要がある.
*1:ただし, caret の train() を併用するという手もある. 少し前のバージョンではうまくいかなかった記憶があるのだが, 現時点の最新版 ver. 6.0-76 では, すべての対応パッケージを試したわけではないが, うまくいくようである. もう少し早くにこの記事を書いておけばよかった.
*2:相互作用項だけがほしいならコロン : を代わりに使う.
*3:2変数がいずれも factor でないなら, 単なる積値になる.
*4:stats::lag()はtsにしか有効でないため無意味であることに注意
*5:蛇足だが, SAS の reg プロシジャ等でもこれに似た構文が使える.
*6:予測値はlog(y)に対するものなので, yに対する予測値が必要ならばexp()で変換する必要がある
*7:先に書いたように caret パッケージと併用すれば formula を利用することができる.
*8:model.frame とは, データフレームにformulaの情報を追加したクラスである
*9:この判定に利用している all.vars() は, formula に含まれる変数名を character 型のベクトルで返す関数である.