備忘ログ

チラシの裏的備忘録&メモ

Rの三項演算子的関数であるifelse()関数(と{dplyr}のif_else()、case_when()関数)で出る謎のwarningメッセージについて

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に論理値を返すオブジェクトつまり条件式など、yestestTRUEの場合に返す値、notestFALSEの場合に返す値を指定する。

今回の例を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 <- testansには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)のそれぞれでyposnposの値の長さを確認し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と一致するため特に意味はない。

ここで、yessqrt(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()関数は条件に一致したものとそうでないものをを分けてから、その後の処理をしているように思えるが、実際にはyesnoのコードを評価後、条件に合わせてベクトルを作るというものになっている。yesnoを評価した時に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
  )
# 後略  
                    }

truefalseをすべて評価している箇所があり、今回の場合はここですべての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 particular NaNs 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以上でなければなりません

yesf(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()系関数を用いると回避できる。