R : if文の使い方。間違えやすいポイントに注意

R

他のプログラミング言語と同様、Rにも条件判定の制御文があります。ただ、Rのif文は、挙動を正しく理解していないと誤った計算をしてしまう場合があります。if文の使い方と間違えやすいポイントについて整理しています。

if文の使い方

他のプログラミング言語同様、Rでも指定した条件で計算処理を切り替えるときにif文が使えます。以下のような説明がされていることが多いです。

if(条件式){
  条件式が真の時の処理
} else {
  条件式が偽の時のの処理
}

(例)絶対値の計算

x <- -1

# xの絶対値を計算する
if(x >=0) {
  y <- x
} else{
  y <- -x
}

print(y)  # y = 1

これはこれで、間違ってはいないのですが、これだけだと間違った計算をしてしまう場合があります。

間違えやすいポイント

先程の絶対値の計算を関数化して、x = -5,-4,…,4,5に対して絶対値を計算してみます。

myfunc <- function(x){ 
if(x >=0){
y <- x
} else{
y <- -x
}
y
}

x <- -5:5
y <- myfunc(x)

#以下のような警告メッセージが出る
# 警告メッセージ:
# if (x >= 0) { で: 条件が長さが 2 以上なので、最初の 1 つだけが使われます
#
print(y) # 5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5
# 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5 ではない

myfunc(x)の実行時に以下のような警告メッセージが出ます。

「 警告メッセージ: if (x >= 0) { で: 条件が長さが 2 以上なので、最初の 1 つだけが使われます 」

また、xの絶対値を計算しているつもりなのに、yには負の値になっているものもあり、想定外の結果になっています。これは、Rのif文の仕様をきちんと理解していないために生じるミスです。

if文についての言語定義

Rの言語定義のドキュメント(https://cran.r-project.org/doc/contrib/manuals-jp/R-lang.jp.v110.pdf)でif文について調べてみると、以下のように記載されています。

3.2.1 if文   

if/else 文は条件に応じて二つの文を評価する。評価される 条件 があり、もしその値 が TRUE なら最初の文が評価され、さもなければ二つ目の文が評価される。if/else 文はその値として選択された文の値を返す。形式的な構文は

if ( statement1 )
statement2
else
statement3

最初に statement1 が評価され value1 を返す。もし value1 が論理値ベクトルでその最初の要素が TRUE なら statement2 が評価される。もし value1 の最初の要素が FALSE なら statement3 が評価される。もし value1 が数値ベクトルなら value1 の最初の要素がゼロなら statement3 が評価され、さもなければ statement2 が評価される。value1 の最初の要素だけが使われる。他の全て の要素は無視される。もし value1 が論理値・数値以外の型を持てば、エラーが起きる。

https://cran.r-project.org/doc/contrib/manuals-jp/R-lang.jp.v110.pdf

ポイントは後半の「最初に statement1 が評価され value1 を返す。もし value1 が論理値ベクトルでその最初の要素が TRUE なら statement2 が評価される。」のところです。つまり、statement2が実行されるかどうかは、statement1の評価結果の最初の要素で決まるということです。絶対値の計算の例の場合だと、x[1]の正負のみで後の処理が決まってしまうのです。上の例だとx[1] < 0なので、すべてxに対してy <- -xが実行され、y = 5, 4, 3,… -4, -5という結果になるのです。

対応策

対応策として、以下の3つの方法が考えられます。

  1. ifelseを使う方法
  2. 論理ベクトルを使う方法
  3. sapplyを使う方法

ifelse関数を使う

Rではベクトルに対応した条件分岐を行える関数(ifelse関数)があります。

ifelse関数の使い方
  ifelse(test, yes, no)
    test   : 条件式
    yes    : testが真の場合の処理
    no     : testが偽の場合の処理
    戻り値  : testと同じ形

絶対値の例だと、以下のようなコードになります。

x <- -5:5
y <- ifelse(x >=0 , x, -x)
print(y)  # 5, 4, 3, 2, 1, 0 , 1, 2, 3, 4, 5

論理ベクトルを使う方法

x >= 0とx < 0のデータに分けるための論理ベクトルを作って、それぞれに対して処理した結果を、戻り値ベクトルの適切な位置に代入します。ちょっと丁寧に関数を書くと以下のようになります。

myfunc2 <- function(x){

  # 戻り値用にxと同じ長さのベクトルを用意する
  y <- vector("double", length(x))

  # x>= 0の評価結果を示す論理ベクトル  
  lg <- (x>=0)  

  z1 <- x[lg]   # x >=0のデータ抽出 (= x[6:11])
  z2 <- x[!lg]  # x < 0のデータ抽出 (= x[1:5])

# x >=0 のデータの対する処理(y[6:11) = z1 = x[6:11])
  y[lg] <- z1     

# x <0 のデータの対する処理(y[1:5] = z2 = x[1:5])
  y[!lg] <- -z2   

  y
}

x <- -5:5
y <- myfunc2(x)
print(y)  # 5, 4, 3, 2, 1, 0 , 1, 2, 3, 4, 5

簡潔に書くのであれば、以下のように書くこともできます。

myfunc2 <- function(x){

  # 戻り値用にxと同じ長さのベクトルを用意する
  y <- vector("double", length(x))

# x >= 0 のデータの対する処理
  y[x >=0] <- x[x >=0]    

# x < 0 のデータの対する処理
  y[x <0] <- -x[x <0]     

  y
}

sapplyを使う方法

if文の条件判定部分にベクトルを使用すると、以下のようなメッセージが出力されます。

「 警告メッセージ: if (x >= 0) { で: 条件が長さが 2 以上なので、最初の 1 つだけが使われます 」

これは、Rが「if文はベクトルに対応してないのに、ベクトル渡すから、最初の1データだけで評価しちゃうよ」と警告しているんです。つまり、関数myfuncがベクトル計算に対応していないとも言えます。そういう場合、sapply()関数を使うことで、形式的にはベクトル化された関数にすることができます。

myfuncを以下のように定義します。基本的には、上で書いた関数と同じですが、内部で1秒待つルーチンを入れているところのみ違います。

myfunc <- function(x){ 
  if(x >=0){
    y <- x
  } else{
    y <- -x
  }
  Sys.sleep(1) # 1秒待つ
  y
}

これに対して、 x = -5〜5としてmyfunc(x)を実行し、計算の実行時間を測ると、先ほどと同じエラーメッセージが出て、実行時間は約1秒かかりる結果になります。

x <- -5:5
system.time({
  y <- myfunc(x)
  print(y)
})

# [1] 5 4 3 2 1 0 -1 -2 -3 -4 -5
# ユーザ システム 経過
# 0.007 0.007 1.001
#
# 警告メッセージ:
# if (x >= 0) { で: 条件が長さが 2 以上なので、最初の 1 つだけが使われます

以下のように、sapply()関数を用いてmyfunc2(x)関数を書くとで、この関数を形式上ベクトル化することができます。

myfunc2 <- function(x){
sapply(x, FUN = myfunc)
}

先ほど同様、 x = -5〜5としてmyfunc2(x)を実行すると、エラーメッセージは出ず、xの絶対値が返ってくるようになります。計算時間は約11秒かかります。myfunc2(x)ではなくabs(x)関数を用いると一瞬で返ってくるのとは大違いです。内部的には計算時間が約1秒かかるmyfunc(x)関数を11回実行しているため時間がかかっています。

x <- -5:5
system.time({
y <- myfunc2(x)
print(y)
})

# [1] 5 4 3 2 1 0 1 2 3 4 5
# ユーザ システム 経過
# 0.058 0.073 11.008

x <- -5:5
system.time({
y <- abs(x)
print(y)
})

# [1] 5 4 3 2 1 0 1 2 3 4 5
# ユーザ システム 経過
# 0.000 0.000 0.001

sapply()関数を使う方法は、ベクトル化できない関数を形式的にベクトル化された関数に書き換えられるので、いろいろな場面で使える方法ではないか思います。ただし、上記のとおり計算スピードが速くなるわけではないので注意してください。

apply系関数の使い方についてはこちらから

R : apply系の関数の使い方
Rではforループを使うと計算速度が遅くなると言われます。でも、apply系の関数を使えば、forループを使わずに、ベクトル、リストなどに対する繰り返し処理を実行でき、計算速度が遅くなることを回避できます。

Rはとても便利な言語なのですが、正しく理解して使わないと、間違った評価になってしまったりもします。matarix型とdata.frame型はどちらも2次元データを保持するデータ型ですが、両者の違いについても理解しておきたいですね

R : matrixとdata.frameの違い
matrixもdata.frameも行、列に数字を並べたもののようですが、ここを曖昧にしてしまうと計算ミスを招きかねない。わかっている人には至極当たり前のことかと思いますが、この記事を読んで僕と同じようにつまずく人が少しでも減ってくれれば幸いです。

 

まとめ

if文でベクトルに対して条件評価をすると、最初の1要素の真偽しか評価されないので注意が必要です。各要素に対して、真偽を評価したい場合は、ifelse()関数、論理ベクトル、sapply()関数を用いる必要があります。

それにしても、Rって、ちょっと癖がありますね。とても面白い言語なんだけど。

R:apply関数をdata.frameに使う場合の注意点
data.frameにapply()関数を適用する場合の注意点をまとめています。apply()関数の使い方をネットで調べても、data.frameに対する使用法はたくさん見つかりますが、注意点が全然出てきません。エラーに悩まされたり、間違った計算をしないように、押さえておくべきところをちゃんと押さえておきましょう。
R : length() 関数が返す長さって何?
length()関数は、長さを返す関数であるということは想像できるのですが、きちんと理解していないと、引数の型によっては期待しているものと違う答えが返ってきます。そして、誤った評価…。そんなことにならないように、きちんと理解しておきましょう。