制御フロー

最終更新日:2024-11-22 | ページの編集

所要時間: 65分

概要

質問

  • Rでデータに依存した選択を行うにはどうすればよいですか?
  • Rで操作を繰り返すにはどうすればよいですか?

目的

  • if...else 文や ifelse() を使って条件分岐文を書く。
  • for() ループを理解し、記述する。

コードを書く際に、特定の条件が満たされた場合にのみアクションを実行したり、特定の回数アクションを実行したりすることがよくあります。

Rでは、制御フローを設定するいくつかの方法があります。条件分岐文の場合、最も一般的に使用される構文は次のとおりです。

R

# if
if (条件が真の場合) {
  アクションを実行
}

# if ... else
if (条件が真の場合) {
  アクションを実行
} else {  # つまり、条件が偽の場合、
  代替のアクションを実行
}

例えば、変数 x が特定の値を持つ場合にメッセージを表示するようにしたいとします。

R

x <- 8

if (x >= 10) {
  print("xは10以上です")
}

x

出力

[1] 8

この場合、x が10以上ではないため、コンソールにメッセージは表示されません。else 文を追加することで、10未満の数値の場合に異なるメッセージを表示できます。

R

x <- 8

if (x >= 10) {
  print("xは10以上です")
} else {
  print("xは10未満です")
}

出力

[1] "xは10未満です"

複数の条件をテストする場合は、else if を使用できます。

R

x <- 8

if (x >= 10) {
  print("xは10以上です")
} else if (x > 5) {
  print("xは5より大きく、10未満です")
} else {
  print("xは5以下です")
}

出力

[1] "xは5より大きく、10未満です"

重要: if() 文内で条件を評価するとき、Rは論理要素(TRUE または FALSE)を探します。これにより、初心者にとって頭痛の種になる場合があります。例えば:

R

x  <-  4 == 3
if (x) {
  "4は3と等しい"
} else {
  "4は3と等しくない"
}

出力

[1] "4は3と等しくない"

この場合、xFALSE であるため、「等しくない」メッセージが表示されます。

R

x <- 4 == 3
x

出力

[1] FALSE

チャレンジ 1

if() 文を使用して、gapminder データセットに 2002 年のレコードがあるかどうかを報告する適切なメッセージを表示してください。 次に、2012 年の場合も同様に行ってください。

チャレンジ 1 の解答は、any() 関数を使用しない方法から見ていきます。 まず、gapminder$year の要素のうち 2002 と等しいものを記述する論理ベクトルを取得します。

R

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

次に、gapminder data.frame の 2002 年に対応する行数をカウントします。

R

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

2002 年のレコードが存在するかどうかは、rows2002_number が 1 以上であることに等しいです。

R

rows2002_number >= 1

これらをまとめると次のようになります:

R

if(nrow(gapminder[(gapminder$year == 2002),]) >= 1){
   print("2002年のレコードが見つかりました。")
}

このすべては、any() を使うことでより簡潔に記述できます。論理条件は次のように表せます:

R

if(any(gapminder$year == 2002)){
   print("2002年のレコードが見つかりました。")
}

次のような警告メッセージが出た人はいますか?

エラー

Error in if (gapminder$year == 2012) {: the condition has length > 1

if() 関数は単一の(長さ 1 の)入力のみを受け付けるため、ベクトルを使用するとエラーを返します。 そのため、if() を使用する際は、入力が単一(長さ 1)であることを確認する必要があります。

ヒント: 組み込みの ifelse() 関数

R は上述のように構成された if()else if() 文を受け付けますが、ifelse() 関数を使用する方法もあります。 この関数は単一およびベクトル入力の両方を受け入れ、次の構文で構成されます:

R

# ifelse 関数
ifelse(条件が真の場合, アクションを実行, 代替アクションを実行)

例えば:

R

y <- -3
ifelse(y < 0, "yは負の数です", "yは正の数または0です")

出力

[1] "yは負の数です"

ヒント: any()all()

any() 関数は、ベクトル内に少なくとも 1 つの TRUE 値がある場合に TRUE を返します。それ以外の場合は FALSE を返します。 これは %in% 演算子と同様の方法で使用できます。 all() 関数はその名前が示す通り、ベクトル内のすべての値が TRUE の場合にのみ TRUE を返します。

繰り返し操作


値のセットを反復処理し、順序が重要で各値に対して同じ操作を実行したい場合、for() ループが役立ちます。 for() ループは シェルのレッスン でも紹介されました。 for() ループは最も柔軟な繰り返し操作のひとつですが、その分、正しく使用するのが難しい場合もあります。 多くの R ユーザーは、for() ループを学ぶことを勧めますが、繰り返しの順序が重要でない場合には使用を避けるようアドバイスしています。 順序が重要でない場合には、purrr パッケージなどのベクトル化された代替手段を学ぶべきです。 これにより、計算効率が向上します。

for() ループの基本構造は次のとおりです:

R

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

例えば:

R

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 はその場で作成されたベクトルです。他のベクトルを使って反復処理することもできます。

2つのことを同時に反復処理するには、別の for() ループをネストして使用できます。

R

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"

出力を見ると、最初のインデックス (i) が 1 に設定されているとき、2 番目のインデックス (j) がその全範囲を反復処理することがわかります。 j のインデックスがすべて処理されると、i がインクリメントされます。このプロセスは各 for() ループで最後のインデックスが使用されるまで続きます。

結果を表示する代わりに、ループの出力を新しいオブジェクトに書き込むこともできます。

R

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" "3 b"
[13] "3 c" "3 d" "3 e" "4 a" "4 b" "4 c" "4 d" "4 e" "5 a" "5 b" "5 c" "5 d"
[25] "5 e"

このアプローチは便利ですが、結果を「増やしていく」(結果オブジェクトを段階的に構築する)ことは計算効率が悪いため、大量の値を反復処理する場合は避けるべきです。

ヒント: 結果を増やさないでください

初心者や経験豊富な R ユーザーがつまずく最大の原因のひとつは、for ループが進むにつれて結果オブジェクト(ベクトル、リスト、マトリックス、データフレームなど)を構築していくことです。 コンピュータはこれを非常に苦手とするため、計算速度が著しく低下する可能性があります。 そのため、適切な次元を持つ空の結果オブジェクトを事前に定義し、各反復で結果を適切な場所に格納するほうがはるかに効率的です。

より効率的な方法は、値を入力する前に(空の)出力オブジェクトを定義することです。 この例では少し複雑に見えますが、それでも効率的です。

R

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" "2 c"
[13] "3 c" "4 c" "5 c" "1 d" "2 d" "3 d" "4 d" "5 d" "1 e" "2 e" "3 e" "4 e"
[25] "5 e"

ヒント: While ループ

特定の条件が満たされている間、操作を繰り返す必要がある場合があります。 これには while() ループを使用します。

R

while(条件が真の間){
  アクションを実行
}

R は条件が満たされていることを「TRUE」として解釈します。

例えば、以下のような while ループは、 runif() 関数を使用して 0 から 1 の範囲のランダムな数を生成し、その数が 0.1 未満になるまで繰り返します。

R

z <- 1
while(z > 0.1){
  z <- runif(1)
  cat(z, "\n")
}

出力

0.5074782
0.3067685
0.4269077
0.6931021
0.08513597 

while() ループは常に適切とは限りません。条件が常に満たされる場合、無限ループに陥ってしまう可能性があるため注意が必要です。

チャレンジ 2

オブジェクト output_vectoroutput_vector2 を比較してください。 それらは同じでしょうか?もし違う場合、その理由は何でしょうか? 最後のコードブロックをどのように変更すれば、output_vector2output_vector と同じにできますか?

2つのベクトルが同一であるかどうかは、all() 関数を使って確認できます:

R

all(output_vector == output_vector2)

ただし、output_vector のすべての要素が output_vector2 内に存在することは確認できます:

R

all(output_vector %in% output_vector2)

逆も同様です:

R

all(output_vector2 %in% output_vector)

したがって、output_vectoroutput_vector2 の要素は異なる順序で並んでいるだけです。 これは、as.vector() が入力マトリックスの列を基に要素を出力するためです。 output_matrix を確認すると、要素を行ごとに取得する必要があることがわかります。

解決策は、output_matrix を転置することです。これには、転置関数 t() を使用するか、要素を適切な順序で入力する方法があります。 最初の解決策では、次のコードを変更します:

R

output_vector2 <- as.vector(output_matrix)

以下のように変更します:

R

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

2つ目の解決策では、次のコードを変更します:

R

output_matrix[i, j] <- temp_output

以下のように変更します:

R

output_matrix[j, i] <- temp_output

チャレンジ 3

gapminder データを大陸ごとにループし、平均寿命が50年より小さいか大きいかを出力するスクリプトを書いてください。

ステップ 1: 大陸ベクトルのすべてのユニークな値を抽出できることを確認します。

R

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

ステップ 2: 各大陸ごとにループして、データの各 subset について平均寿命を計算する必要があります。 次のように行えます:

  1. ‘continent’ のユニークな値ごとにループする。
  2. 各大陸の値について、そのサブセットを一時変数に格納する。
  3. 計算された平均寿命をユーザーに出力として返す:

R

for (iContinent in unique(gapminder$continent)) {
  tmp <- gapminder[gapminder$continent == iContinent, ]
  cat(iContinent, mean(tmp$lifeExp, na.rm = TRUE), "\n")
  rm(tmp)
}

ステップ 3: この課題では、平均寿命が50未満または50以上の場合のみ出力する必要があります。 そのため、計算された平均寿命が閾値より上か下かを評価し、結果に応じて出力を行う if() 条件を追加します。 以下に(3)を修正したものを示します:

3a. 計算された平均寿命が閾値 (50年) より小さい場合は大陸名と「50未満」を、閾値以上の場合は大陸名と「50以上」を返します。

R

thresholdValue <- 50

for (iContinent in unique(gapminder$continent)) {
   tmp <- mean(gapminder[gapminder$continent == iContinent, "lifeExp"])

   if (tmp < thresholdValue){
       cat("平均寿命は", iContinent, "で", thresholdValue, "年未満です\n")
   } else {
       cat("平均寿命は", iContinent, "で", thresholdValue, "年以上です\n")
   } # if else 条件の終了
   rm(tmp)
} # for ループの終了

チャレンジ 4

チャレンジ 3 のスクリプトを変更して、今度は各国ごとにループを行います。 この際、平均寿命が50年未満、50年から70年の間、または70年を超えているかを出力してください。

チャレンジ 3 の解答を修正し、lowerThresholdupperThreshold という2つの閾値を追加し、if-else 文を拡張します:

R

 lowerThreshold <- 50
 upperThreshold <- 70

for (iCountry in unique(gapminder$country)) {
    tmp <- mean(gapminder[gapminder$country == iCountry, "lifeExp"])

    if(tmp < lowerThreshold) {
        cat("平均寿命は", iCountry, "で", lowerThreshold, "年未満です\n")
    } else if(tmp > lowerThreshold && tmp < upperThreshold) {
        cat("平均寿命は", iCountry, "で", lowerThreshold, "年から", upperThreshold, "年の間です\n")
    } else {
        cat("平均寿命は", iCountry, "で", upperThreshold, "年以上です\n")
    }
    rm(tmp)
}

チャレンジ 5 - 応用

gapminder データセットの各国ごとにループし、その国が「B」で始まるかどうかをテストし、 平均寿命が50年未満の場合には、時間に対する寿命の折れ線グラフを作成するスクリプトを書いてください。

「B」で始まる国を見つけるには、Unix シェルのレッスン で紹介された grep() コマンドを使用します。 まず以下を試します:

R

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

ただし、このコマンドは「B」で始まる country のファクター変数のインデックスを返します。 値を取得するには、value=TRUE オプションを grep() コマンドに追加する必要があります:

R

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

これらの国を candidateCountries という変数に格納し、その変数内の各エントリをループします。 ループ内で各国の平均寿命を評価し、平均寿命が50未満の場合、with()subset() を使用して寿命の推移をプロットします:

R

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

for (iCountry in candidateCountries) {
    tmp <- mean(gapminder[gapminder$country == iCountry, "lifeExp"])

    if (tmp < thresholdValue) {
        cat("平均寿命は", iCountry, "で", thresholdValue, "年未満です。寿命グラフを描画中...\n")

        with(subset(gapminder, country == iCountry),
                plot(year, lifeExp,
                     type = "o",
                     main = paste(iCountry, "の寿命の推移"),
                     ylab = "平均寿命",
                     xlab = "年"
                     ) # plot の終了
             ) # with の終了
    } # if の終了
    rm(tmp)
} # for ループの終了

まとめ

  • ifelse を使用して選択肢を作る。
  • for を使用して操作を繰り返す。