実行の制御

概要

講義: 45 分
演習: 20 分
質問
  • R でデータに依存した選択を行うにはどうすればよいですか?

  • R で処理を繰り返すにはどうすればよいですか?

目標
  • if()else() で条件文が書けるようになりましょう。

  • for() ループを理解して使えるようになりましょう。

コードを書く際、実行の流れを制御する必要がよくあります。 これは、ある条件、または一連の条件が満たされたときに実行されるようにすればできます。 あるいは、決まった回数実行されるよう設定することもできます。

Rでは、流れを制御する方法がいくつかあります。 条件付きの宣言で、最もよく使われるのが構文(constructs)です:

# if
if (condition is true) {
  perform action
}

# if ... else
if (condition is true) { # 条件が満たされた場合
  アクションを行う
} else {  # つまり、条件が満たされなかった場合
  別のアクションを行う
}

例えばRに、もし変数 x が特定の値を持っていた場合、メッセージを表示させたいとします。

x <- 8

if (x >= 10) {
  print("x is greater than or equal to 10")
}

x
[1] 8

xは10より大きくないため、表示された宣言はコンソール上に表示されません。10より小さい場合に違ったメッセージを表示させるためには、 else 宣言を追加しなければなりません。

x <- 8

if (x >= 10) {
  print("x is greater than or equal to 10")
} else {
  print("x is less than 10")
}
[1] "x is less than 10"

else if を使うと、複数の条件を試すこともできます。

x <- 8

if (x >= 10) {
  print("x is greater than or equal to 10")
} else if (x > 5) {
  print("x is greater than 5, but less than 10")
} else {
  print("x is less than 5")
}
[1] "x is greater than 5, but less than 10"

重要: Rが if() 宣言の中の条件を計算する際、論理要素( TRUEFALSE )を探します。 初心者には複雑なので、混乱してしまうかもしれません。例えば:

x  <-  4 == 3
if (x) {
  "4 equals 3"
} else {
  "4 does not equal 3"          
}
[1] "4 does not equal 3"

ここで見られるように、ベクトル x が FALSE であるため、不等号のメッセージが表示されました。

x <- 4 == 3
x
[1] FALSE

チャレンジ1

if() 宣言を使って、gapminder のデータセットに2002年の記録があるかどうかを伝える 適当なメッセージを表示してください。 2012年についても、同じことをしてください。

チャレンジ1の解答

any() を使わないチャレンジ1の解答をまず見てみましょう。 最初に、gapminder$year の内、どの要素が 2002 と等しいかを記した論理ベクトルを手に入れます:

gapminder[(gapminder$year == 2002),]

そして、データフレーム gapminder の2002と対応する行の数を数えます:

rows2002_number <- nrow(gapminder[(gapminder$year == 2002),])

2002年の記録が存在するということは、 rows2002_number が1以上あるということです:

rows2002_number >= 1

全てを合わせると、以下になります:

if(nrow(gapminder[(gapminder$year == 2002),]) >= 1){
   print("Record(s) for the year 2002 found.")
}

any() を使えば、もっと早くできます。論理条件は、次のように書けます:

if(any(gapminder$year == 2002)){
   print("Record(s) for the year 2002 found.")
}

次のような警告メッセージをもらった人はいますか?

Warning in if (gapminder$year == 2012) {: the condition has length > 1 and
only the first element will be used

もし、1つ以上の要素を持つベクトルを評価する条件の場合、 関数 if() は実行されるのですが、最初の要素の条件しか評価しません。 ここでは、確実に条件の長さは1であるようにする必要があります。

ヒント:any()all()

any() 関数は、ベクトルの中に少なくとも1つ TRUE の値がある場合、 TRUE を返し、 そうでない場合は、 FALSE を返します。 これは、 %in% 演算子でも同様に使えます。 関数 all() は、その名前が示唆しているように、ベクトル内の全ての値が TRUE である時のみ、 TRUE となります。

繰り返し行う処理

一連の値に繰り返し同じ演算をしたい場合、 そして繰り返す順番も重要である場合は、 for() ループを使いましょう。 for() ループは、先ほどシェルのレッスンで見ましたね。これは、最も柔軟な ループの演算子ですが、それゆえ、正しく使うのが最も難しいです。 繰り返す順番が重要(つまり直前の繰り返しの結果をもとに、次 の演算を行う場合)でない限り、 for() ループを使うのを避けましょう。

for() ループの基本構造は:

for(iterator in set of values){
  do a thing
}

例えば:

for(i in 1:10){
  print(i)
}
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
[1] 6
[1] 7
[1] 8
[1] 9
[1] 10

1:10 の部分は、ベクトルをその場で作るものです。 他のベクトルの中身を繰り返すこともできます。

for() ループを、もうひとつの for() ループと入れ子となる形にすれば、 2つ同時に繰り返すこともできます。

for(i in 1:5){
  for(j in c('a', 'b', 'c', 'd', 'e')){
    print(paste(i,j))
  }
}
[1] "1 a"
[1] "1 b"
[1] "1 c"
[1] "1 d"
[1] "1 e"
[1] "2 a"
[1] "2 b"
[1] "2 c"
[1] "2 d"
[1] "2 e"
[1] "3 a"
[1] "3 b"
[1] "3 c"
[1] "3 d"
[1] "3 e"
[1] "4 a"
[1] "4 b"
[1] "4 c"
[1] "4 d"
[1] "4 e"
[1] "5 a"
[1] "5 b"
[1] "5 c"
[1] "5 d"
[1] "5 e"

結果を表示させずに、ループの結果を新しいオブジェクトに書き込むこともできます。

output_vector <- c()
for(i in 1:5){
  for(j in c('a', 'b', 'c', 'd', 'e')){
    temp_output <- paste(i, j)
    output_vector <- c(output_vector, temp_output)
  }
}
output_vector
 [1] "1 a" "1 b" "1 c" "1 d" "1 e" "2 a" "2 b" "2 c" "2 d" "2 e" "3 a"
[12] "3 b" "3 c" "3 d" "3 e" "4 a" "4 b" "4 c" "4 d" "4 e" "5 a" "5 b"
[23] "5 c" "5 d" "5 e"

このアプローチが役に立つこともありますが、’結果を太らせる’ (結果のオブジェクトを 徐々に積み上げる)と、演算する上で非効率になります。 ゆえに、多くの値の間を繰り返すときは避けましょう。

ヒント:結果を太らせないようにしましょう

初心者、そして経験のあるRユーザーがよくつまずいてしまう ループの使い方の一つが、結果のオブジェクト (ベクトル、リスト、行列、データフレームなど)をループするたびに作ってしまうことです。 コンピュータはこれを扱うのがとても下手で、すぐに計算が 遅くなってしまいます。適当な次元を持つ空の結果オブジェクトを 前もって宣言しておく方が効率的です。 もし、上記のように、最後の結果が行列に蓄積されるのを知っていたら、 5行、5列の数列を作成しておいて、それぞれの繰り返しで適切な場所へ結果を 保存しましょう。

よりよい方法は、(空の)出力オブジェクトを、値を埋める前に宣言することです。 この例では、より複雑に見えますが、より効率的です。

output_matrix <- matrix(nrow=5, ncol=5)
j_vector <- c('a', 'b', 'c', 'd', 'e')
for(i in 1:5){
  for(j in 1:5){
    temp_j_value <- j_vector[j]
    temp_output <- paste(i, temp_j_value)
    output_matrix[i, j] <- temp_output
  }
}
output_vector2 <- as.vector(output_matrix)
output_vector2
 [1] "1 a" "2 a" "3 a" "4 a" "5 a" "1 b" "2 b" "3 b" "4 b" "5 b" "1 c"
[12] "2 c" "3 c" "4 c" "5 c" "1 d" "2 d" "3 d" "4 d" "5 d" "1 e" "2 e"
[23] "3 e" "4 e" "5 e"

ヒント:while ループ

時には、ある条件が満たされるまで繰り返す必要があります。 これは、 while() ループを使えばできます。

while(this condition is true){
  do a thing
}

例として、このwhileループは 一様分布(runif() 関数)から0.1よりも小さい数を得るまで、 0から1の間で乱数を生成します。

z <- 1
while(z > 0.1){
  z <- runif(1)
  print(z)
}

while() ループは、いつも適当であるとは言えません。特に、条件が決して満たされないことによる 無限ループに陥らないように注意する必要があります。

チャレンジ2

output_vectoroutput_vector2 のオブジェクトを比較してみましょう。 同じですか?もし違ったら、なぜそうなったのでしょう? output_vector2 の最後のコードのかたまりを output_vector と同じにするには、どう変えればいいのでしょうか?

チャレンジ2の解答

2つのベクトルが同じかを調べるために、 all() 関数を使いましょう:

all(output_vector == output_vector2)

しかし、 output_vector の全ての要素は、 output_vector2 にあります:

all(output_vector %in% output_vector2)

そして、その逆もしかり。

all(output_vector2 %in% output_vector)

ですので、output_vectoroutput_vector2 の要素は、違う順番で蓄積されただけみたいです。 理由は、 as.vector() は列ごとに行列から要素を出力するからです。 output_matrix を見てみると、要素を行ごとに出力して欲しいことに気づくでしょう。 解決方法は、 output_matrix を転置することです。転置関数 t() を使うか、 要素を正しい順番で入力するか、いずれかの方法で対応可能です。 最初の解決方法は、元の形である

output_vector2 <- as.vector(output_matrix)

を、次のように変えないといけません:

output_vector2 <- as.vector(t(output_matrix))

二番目の解決方法は、以下を:

output_matrix[i, j] <- temp_output

次のように変えます:

output_matrix[j, i] <- temp_output

チャレンジ3

gapminder データを大陸ごとにループし、平均余命が50歳以上かどうかを表示する スクリプトを書きましょう。

チャレンジ3の解答

手順1:大陸ベクトルから、確実に全ての固有の値を抜き出せるかを確かめましょう。

gapminder <- read.csv("data/gapminder_data.csv")
unique(gapminder$continent)

手順2:これらの大陸のそれぞれにループをし、その 部分集合 データごとに平均余命を出す必要があります。 それは次のようにすればできます:

  1. ‘大陸(continent)’ の固有の値のそれぞれについてループする
  2. 大陸のそれぞれの値ごとに、部分集合の平均余命を蓄積する一時的な変数を作る
  3. 計算した平均余命を返し、 出力を表示させる:
for( iContinent in unique(gapminder$continent) ){
   tmp <- mean(subset(gapminder, continent==iContinent)$lifeExp)
   cat("Average Life Expectancy in", iContinent, "is", tmp, "\n")
   rm(tmp)
}

手順3:この演習では、平均余命が50歳以上かどうかの結果だけを出力するものでした。 ゆえに、結果を表示させる前に if 条件をつけて、演算された平均余命が基準値以上か、基準値未満かを判別し、結果によって正しい出力を表示させる必要があります。 これを踏まえて、上の (3) を修正する必要があります:

3a. もし計算された平均余命が、ある基準(50歳)未満の場合、大陸と平均余命は基準未満であるという宣言を、そうでない場合は、大陸と平均余命は基準値以上であるという宣言を返す:

thresholdValue <- 50

for( iContinent in unique(gapminder$continent) ){
   tmp <- mean(subset(gapminder, continent==iContinent)$lifeExp)
   
   if(tmp < thresholdValue){
       cat("Average Life Expectancy in", iContinent, "is less than", thresholdValue, "\n")
   }
   else{
       cat("Average Life Expectancy in", iContinent, "is greater than", thresholdValue, "\n")
        } # end if else condition
   rm(tmp)
   } # end for loop

チャレンジ4

チャレンジ3のスクリプトをそれぞれの国ごとにループする形に直してください。 今回は、平均余命は50歳未満か、50歳以上70歳未満か、70歳以上かを 表示しましょう。

チャレンジ4の解答

チャレンジ3の解答を、 lowerThresholdupperThreshold の2つの基準値を加え、if-else 宣言を拡張する形で修正します:

 lowerThreshold <- 50
 upperThreshold <- 70
 
for( iCountry in unique(gapminder$country) ){
    tmp <- mean(subset(gapminder, country==iCountry)$lifeExp)
    
    if(tmp < lowerThreshold){
        cat("Average Life Expectancy in", iCountry, "is less than", lowerThreshold, "\n")
    }
    else if(tmp > lowerThreshold && tmp < upperThreshold){
        cat("Average Life Expectancy in", iCountry, "is between", lowerThreshold, "and", upperThreshold, "\n")
    }
    else{
        cat("Average Life Expectancy in", iCountry, "is greater than", upperThreshold, "\n")
    }
    rm(tmp)
}

チャレンジ5 - 上級

gapminder データセットで、それぞれの国ごとにループするスクリプトを書き、 国が ‘B’ で始まるかどうかをテストし、平均余命が50歳以下の場合、平均余命を 時間ごとの移り変わりで示した線グラフを書きましょう。

チャレンジ5の解答

シェルのレッスンで紹介したgrep コマンドを「B」で始まる国を見つけるために使います。 まず、これをどうやって使うかを理解しましょう。 シェル節に従って、以下を試してみようと思うかもしれません:

grep("^B", unique(gapminder$country))

でも、このコマンドを演算すると、順序なし因子変数 country の「B」で始まる要素番号を返します。 値を得るためには、 grep コマンドのvalue=TRUE オプションを加える必要があります:

grep("^B", unique(gapminder$country), value=TRUE)

これらの国々をcandidateCountriesと呼ぶ変数に蓄積し、その変数のそれぞれをループするようにしましょう。 そのループの中で、それぞれの国の平均余命を演算子、もし平均余命が50歳未満でったら、平均余命の進展をみるために、with()subset()を使い、base-plotを用いてプロットしましょう:

thresholdValue <- 50
candidateCountries <- grep("^B", unique(gapminder$country), value=TRUE)

for( iCountry in candidateCountries){
    tmp <- mean(subset(gapminder, country==iCountry)$lifeExp)
    
    if(tmp < thresholdValue){
        cat("Average Life Expectancy in", iCountry, "is less than", thresholdValue, "plotting life expectancy graph... \n")
        
        with(subset(gapminder, country==iCountry),
                plot(year,lifeExp,
                     type="o",
                     main = paste("Life Expectancy in", iCountry, "over time"),
                     ylab = "Life Expectancy",
                     xlab = "Year"
                   ) # end plot
              ) # end with
    } # end for loop
    rm(tmp)
 }```
> {: .solution}
{: .challenge}

まとめ

  • 選択の際は ifelse を使う。

  • 繰り返す処理には for を使う。