Rでデータ分析をする際に、三項演算子的な処理を行いたい時に{base}
のifelse()
関数や{dplyr}
パッケージのif_else()
、もっと複雑な条件分岐をするときにはcase_when()
関数を使うことがある。これらの関数を使っている時に意図しない、結果にも反映されない謎のwarningメッセージがでることがある(場合によっては謎のerrorで処理がストップする)。この謎のwarningメッセージはどいうもので、どういう原因で出てくるのかを、特にifelse()
の内部処理を追いかけながら見ていく。謎warningメッセージと書いたが、実質仕様なので仕組みが分かれば謎ではなくなる。
まず謎のwarningメッセージについて例を示しておく。次のような場合、ifelse()
などではwarningメッセージが表示される。
y <- seq(-2, 2, by = 0.5) ifelse(y >= 0, sqrt(y), y)
## Warning in sqrt(y): 計算結果が NaN になりました
## [1] -2.0000000 -1.5000000 -1.0000000 -0.5000000 0.0000000 0.7071068 1.0000000
## [8] 1.2247449 1.4142136
warningメッセージ(警告)では「計算結果がNaNとなりました」と表示されているが、出力されている結果にはNaN
がなく一見すると謎のwarningメッセージとなっている。こういった、出力された結果からみるとなにのことを行っているのかわからないwarningメッセージを謎のwarningメッセージとする。もし、この謎のwarningメッセージが出力される原因などを知っているのであれば以下本稿はそのことしか話をしていないので読む必要はまったくない。
次から具体的な例を上げながらみていく。
ifelse()
の処理を追っていく
-2から2まで、0.5刻みの値のベクトルについて、これの各値の平方根を求めようと考えてみる。
-2から2までの0.5刻みの値のベクトルと、それを平方根を求めるsqrt()
関数で処理すると次のような結果になる。
y <- seq(-2, 2, by = 0.5) y
## [1] -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0
sqrt(y)
## Warning in sqrt(y): 計算結果が NaN になりました
## [1] NaN NaN NaN NaN 0.0000000 0.7071068 1.0000000
## [8] 1.2247449 1.4142136
warningメッセージで「計算結果がNaNになりました」と表示されているが、0未満の値は実数範囲で平方根を計算できないために警告がでている。
ちなみに本稿の本旨とは異なるが、計算範囲を複素数範囲まで拡張するためには与える値を複素数範囲で与えれば計算でき、warningメッセージは表示されない。
x <- complex(real = seq(-2, 2, by = 0.5)) x
## [1] -2.0+0i -1.5+0i -1.0+0i -0.5+0i 0.0+0i 0.5+0i 1.0+0i 1.5+0i 2.0+0i
sqrt(x)
## [1] 0.0000000+1.4142136i 0.0000000+1.2247449i 0.0000000+1.0000000i
## [4] 0.0000000+0.7071068i 0.0000000+0.0000000i 0.7071068+0.0000000i
## [7] 1.0000000+0.0000000i 1.2247449+0.0000000i 1.4142136+0.0000000i
ここで、0以上か0未満かで条件分けし0以上であれば平方根を取り、それ以外の場合は元々の値を返すようにしてみる(それ以外の場合の値をNA
などにしてもよいが、今回はちゃんと判別されて計算されていることを確認したいため元の値を返すようにしてみる)。
この場合、sapply()
関数、if()
、else()
関数を用いると次のように各値を0以上か0未満か判別して計算することができる。
sapply(y, function(y){ if(y >= 0) sqrt(y) else y })
## [1] -2.0000000 -1.5000000 -1.0000000 -0.5000000 0.0000000 0.7071068 1.0000000
## [8] 1.2247449 1.4142136
この場合は意図した通り、0未満は元々の値、0以上では各値の平方根の値が出力され、warningメッセージは出力されない。
ここで{base}
のifelse()
を使っても同じようなものが実現できそうに思える。ifelse()
関数はエクセルのIF関数と似た引数で、ifelse(test, yes , no)
でtest
に論理値を返すオブジェクトつまり条件式など、yes
にtest
がTRUE
の場合に返す値、no
にtest
がFALSE
の場合に返す値を指定する。
今回の例をifelse()
を使って単純に書くと次のようになる(冒頭と同じ)。
ifelse(y >= 0, sqrt(y), y)
## Warning in sqrt(y): 計算結果が NaN になりました
## [1] -2.0000000 -1.5000000 -1.0000000 -0.5000000 0.0000000 0.7071068 1.0000000
## [8] 1.2247449 1.4142136
これはwarningメッセージ(警告)がでるが、「計算結果がNaN
になりました」と言われる一方で計算結果はちゃんと意図した通り、0未満では元々の値が帰ってきておりNaN
という値は出力されていないので一見すると謎のwarningメッセージに見える。
これはifelse()
がどういう処理をしているのか(ソースコードをざっと)みると判明する。今回追いかける処理の部分は次の通り。
ifelse <- function(test, yes, no){ # 中略 ans <- test len <- length(ans) ypos <- which(test) npos <- which(!test) if (length(ypos) > 0L) ans[ypos] <- rep(yes, length.out = len)[ypos] if (length(npos) > 0L) ans[npos] <- rep(no, length.out = len)[npos] ans }
今回のようなifelse(y >= 0, sqrt(y), y)
のような単純な例であればここだけでifelse()
関数は実行されている。
順に追っていく。
ans <- test
でans
にはtest
の結果、論理値のベクトルが格納される。今回の場合はFALSE,
FALSE, FALSE, FALSE, TRUE, TRUE, TRUE, TRUE,
TRUEが格納されている。次に、len <- lentgh(ans)
で最終的に返す答えの長さを調べ、格納している(今回の場合は特に問題にならない)。
次に、ypos <- which(test)
とnpos <- which(!test)
について。which()
関数は論理ベクトルのTRUE
の場所インデックス、つまりベクトルの先頭を1としたときに先頭から何番目がTRUE
かを示してくれる。つまり、ypos <- which(test)
では5,
6, 7, 8,
9がypos
に格納される。test
に!
がついているnpos
はそれが逆転し、test
の条件で考えると先頭を1としたときにに先頭から何番目がFALSE
かを調べており、npos <- which(!test)
では1,
2, 3, 4がnpos
に格納されている。
次の
if (length(ypos) > 0L) ans[ypos] <- rep(yes, length.out = len)[ypos] if (length(npos) > 0L) ans[npos] <- rep(no, length.out = len)[npos]
では、length(ypos)
とlength(npos)
のそれぞれでypos
とnpos
の値の長さを確認し0より大きい場合にはその次に続く関数を実行することとなっている。今回はどちらも長さが0よりも大きいのでどちらも実行されている。
ypos
の長さが0よりも大きい場合に実行される、ans[ypos] <- rep(yes, length.out = len)[ypos]
は、ans
のインデックスがypos
のところにyes
の結果のインデックス(先頭からの位置)がypos
の値で上書きされることとなる。今回、ypos
は5,
6, 7, 8, 9となっているので、ans
の先頭から5, 6, 7, 8,
9番目の値が、yes
の結果の先頭から5, 6, 7, 8,
9番目の値で上書きされる。rep()
関数はyes
の長さがtest
の長さと異なる時に調整するもので、今回の場合yes
は長さがtest
と一致するため特に意味はない。
ここで、yes
はsqrt(y)
になっているので、すべてのsqrt(y)
を評価しているので冒頭に示したようにwarningメッセージを出しつつ結果NaN,
NaN, NaN, NaN, 0, 0.7071068, 1, 1.2247449,
1.4142136を返す。ここでコンソール上に表示される謎のwarningメッセージがでてくる。ans
に格納するために使う値はyes
の中でypos
でインデックスがちゃんと指定されているのでNaN
は取り出されない。
次にnpos
の長さが0よりも大きい場合に実行される、ans[npos] <- rep(no, length.out = len)[npos]
では、ans
のインデックスがnpos
のところにno
の結果のインデックス(先頭からの位置)がnpos
の値で上書きされることとなる。つまり、先程のans[ypos] <- rep(yes, length.out = len)[ypos]
で上書きされなかった箇所がno
の値で置き換えられる。今回の例でnpos
は1,
2, 3, 4となっているので、ans
の先頭から1, 2, 3,
4番目の値が、no
の結果、今回の場合はy
そのものなので-2, -1.5, -1,
-0.5, 0, 0.5, 1, 1.5, 2の先頭から1, 2, 3,
4番目の値で上書きされることとなる。
そして作成されたans
ベクトルが最終的に結果として出力されることとなる。
上記のように処理を追うと、最終的な結果と関係しない謎のwarningがどこででているのかわかる。
その証拠に次のように意図的にwarningメッセージを2つ出すこともできる。
# 絶対値の平方根をとる ifelse(y >= 0, sqrt(y), sqrt(-y))
## Warning in sqrt(y): 計算結果が NaN になりました
## Warning in sqrt(-y): 計算結果が NaN になりました
## [1] 1.4142136 1.2247449 1.0000000 0.7071068 0.0000000 0.7071068 1.0000000
## [8] 1.2247449 1.4142136
一見するとifelse()
関数は条件に一致したものとそうでないものをを分けてから、その後の処理をしているように思えるが、実際にはyes
、no
のコードを評価後、条件に合わせてベクトルを作るというものになっている。yes
やno
を評価した時にwarningメッセージが出る場合には、出力結果に問題なくても謎のwarningメッセージが出ることがありえる。
{dplyr}
のifelse()
とcase_when()
の場合を簡単に
ところで、ifelse()
とほぼ同等の関数が{dplyr}
であればif_else()
関数としてある。
dplyr::if_else(y >= 0, sqrt(y), y)
## Warning in sqrt(y): 計算結果が NaN になりました
## [1] -2.0000000 -1.5000000 -1.0000000 -0.5000000 0.0000000 0.7071068 1.0000000
## [8] 1.2247449 1.4142136
と同じように謎warningメッセージがでる。
これも処理を簡単に追っていくと
if_else <- function(condition, true, false, missing = NULL, ..., ptype = NULL, size = NULL) { # 中略 values <- list( true = true, false = false ) # 後略 }
とtrue
とfalse
をすべて評価している箇所があり、今回の場合はここですべてのy
に対してsqrt(y)
が実行されここでwarningメッセージが出ている。
同じく、case_when()
も次のようになる。
dplyr::case_when(y >= 0 ~ sqrt(y), .default = y)
## Warning in sqrt(y): 計算結果が NaN になりました
## [1] -2.0000000 -1.5000000 -1.0000000 -0.5000000 0.0000000 0.7071068 1.0000000
## [8] 1.2247449 1.4142136
これもこれまで同様に、途中処理の途中で、右辺(sqrt(y)
)が評価されているため、一見すると謎のwarningメッセージが表示されることとなる。
{dplyr}
のcase_when()
ではヘルプドキュメントのexamplesのところに、
case_when()
evaluates all RHS expressions, and then constructs its result by extracting the selected (via the LHS expressions) parts. In particularNaN
s are produced in this case:
と右辺すべて評価しているとかかれている(今回の-2から2までの平方根をとるという例はここにある)。
特に問題になる場合
今回の例のように、単純な例であれば出ているwarningメッセージはただ謎なだけで、「結果は問題ないが?」程度の疑問で終わる(warnigメッセージが出ているという気持ちの悪さは残る)がもう少し複雑な場合や、処理がerrorでストップする例がある場合には大きな問題になる。
例えば、次のように、0以上であれば与えられた値を返し、0未満errorを返すf()
という関数を考え、ifelse()
で値が0以上であればf()
で処理した値、0未満の場合にはそのままの値を返すような処理を想定すると、次のようになる。
f <- function(x){ sapply(x, function(x){ if(x >= 0) x else stop("xは0以上でなければなりません") }) } x <- -2:2 ifelse(x >= 0, f(x), x)
## Error in FUN(X[[i]], ...): xは0以上でなければなりません
yes
のf(x)
が評価されているので、そこでerrorがでるとそこで処理がストップする。なんでどうしてerrorがでるのかわからない場合には、「?」となってしまうと思う。
この場合は、やはり単純に解決するためには、sapply()
などを使って一つつづの値で処理ができるようにしていくのがいいと思う。
sapply(x, function(x){ if(x >= 0) f(x) else x })
## [1] -2 -1 0 1 2
# ifelse()をこの中で使うこともできる sapply(x, function(x) ifelse(x >= 0, f(x), x))
## [1] -2 -1 0 1 2
最後に
ifelse()
やif_else()
、case_when()
関数で処理した後に文字列や任意の値にするなどの固定の値であれば、特に上記のような問題は起こらないと思う。
一方で、条件にマッチしたら値をその値を処理するなどの方法を行うと、気をつけていないと踏んでしまう問題だと思う。
上記のようにsapply()
や他には{purrr}
のmap()
系関数を用いると回避できる。