11 Rプログラミングの基礎
この章では統計ソフトウェアではなく、プログラミング言語としてのRについて解説する。プログラミングは難しいというイメージがあるかもしれないが、実は難しい。しかし、プログラミングをする場合にまず覚えるべき重要概念は、
- データの読み書き1
- 条件分岐
- 反復
の3つだけだ2。この3つだけでほとんどのプログラムが作れる。ただ、この単純さがプログラミングの難しさもあるとも言える。
たとえば、ある数字の列を小さい順(昇順)に並び替えることを考えよう。c(6, 3, 7, 2, 5, 1, 8, 4) という数列が与えられたとき、人間ならあまり苦労することなく、c(1, 2, 3, 4, 5, 6, 7, 8) に並び替えられるだろう。しかし、その手順を「代入」、「条件分岐」、「反復」のみで明確に表現できるだろうか3。もちろん、できる。よって、並べ替えはプログラミングによって実現することができる。Rには、数字を並べ替えるためのsort()関数やorder()関数などが用意されており、それらの組込関数を使えば良いのだが、組込関数は「代入」、「条件分岐」、「反復」を組み合わせて作られたものだ。
「代入」、「条件分岐」、「反復」という3つの概念さえ理解すれば何でもできるるという意味で、プログラミングは簡単だ。しかし、この3つだけで解決しないといけないという意味で、プログラミングは難しい。頻繁に利用する機能については、ほとんどのプログラミング言語があらかじめ作られた関数 (built-in function, 組込関数) を数多く提供しており、ユーザである私たちは組込関数を使うのが賢明である。それでも条件分岐や反復について勉強する必要があるのは、私たち一般ユーザにとってもこれが必要な場面が多いからである。たとえば、同じ分析をデータだけ変えながら複数回実行するために反復を利用する。反復をうまく使えば、コードの量を数十分の一に減らすことも可能である。また、特定の条件に応じてデータを分類するときは条件分岐を使う。条件分岐を使いこなせないと、CalcやExcelなどのスプレッドシートでデータを目でみながら値を入力するという苦行を強いられるが4、条件分岐が使えればそのような作業は必要ない。数行のプログラミングをするだけで、ほんの数秒で分類が終わる。
11.1 R言語の基礎概念
11.1.1 オブジェクト
これまで「オブジェクト」や「関数」などという概念を定義せずに使ってきたが、ここではR言語の構成する基本的な概念についてもう少し詳細に解説する。これらは、プログラミングをする上である程度は意識的に使う必要があるものである。
まず、オブジェクト (object) とはメモリに割り当てられた「何か」である。「何か」に該当するのは、ベクトル (vector)、行列 (matrix)、データフレーム (data frame)、リスト (list)、関数 (function) などである。一般的に、オブジェクトにはそれぞれ固有の(つまり、他のオブジェクトと重複しない)名前が付いている。
たとえば、1から5までの自然数の数列を
のようにmy_vec1という名前のオブジェクトに格納する。オブジェクトに名前をつけてメモリに割り当てると、その後 my_vec1 と入力するだけでそのオブジェクトの中身を読み込むことができるようになる。
ここで、次のように my_vec1の要素を2倍にする操作を考えてみよう。
my_vec1は、先ほど定義したオブジェクトである。では2はどうだろうか。2はメモリに割り当てられていないので、オブジェクトではないのだろうか。実は、この数字 2 もオブジェクトである。計算する瞬間のみ2がメモリに割り当てられ、計算が終わったらメモリから消されると考えれば良い。「Rに存在するあらゆるものはオブジェクトである (Everything that exists in R is an object)」 (Chambers 2016)。* のような演算子でさえもオブジェクトである。
11.1.2 クラス
クラス (class) とはオブジェクトを特徴づける属性である。既に何度か class() 関数を使ってデータ型やデータ構造を確認したが、class()関数でオブジェクトのクラスを確認することができる。先ほど、my_vec1も*も2もオブジェクトであると説明した。これらがすべてオブジェクトであるということは、何らかのクラス属性を持っているというこである。また、class()関数そのものもオブジェクトなので、何らかのクラスをもつ。確認してみよう。
[1] "numeric"
[1] "function"
[1] "numeric"
[1] "function"
統計分析をする際に、Rのクラスを意識することはあまりない。しかし、Rでパッケージを開発したり、複雑な関数を自作する場合、オブジェクト指向プログラミング (Object-oriented Programming; OOP) の考え方が重要で、オブジェクトのクラスを厳密に定義する必要がある5。
自分でパッケージを開発しないとしても、クラスの概念は知っておいたほうが良い。この後説明する関数には引数というものがあるが、引数には特定のクラスのオブジェクトしか指定できない。関数の使い方がわからないときには?関数名 でヘルプを表示するが、ヘルプには各引数に使うべきクラスが書かれている。クラスについて何も知らないと、ヘルプを読んでも意味がわからないかもしれない。
たとえば、Rコンソール上で?meanを入力して実行すると、 図 11.1 のような mean() 関数のヘルプが表示される。ヘルプに示されているとおり、mean()関数に必要な引数は x、trim、na.rmである。仮引数xの説明を読むと、x の実引数にはnumeric または logical のベクトルクラスが使えることが分かる。さらに、date、date-time、time interval が使えることも教えてくれる。
mean()関数のヘルプ画面
回帰分析を行うlm()関数の場合、data という仮引数があるが、dataの実引数に指定できるのは data.frame クラスまたは tibbleクラスのオブジェクトである6。通常はクラスを意識せずにRを使っても問題ないが、
- 全てのオブジェクトにはクラスが付与されており、
- 関数 (の引数) ごとに実引数として使えるクラスが異なる
という2点は覚えておこう。
11.1.3 関数と引数
関数 (function) は、入力されたデータを内部で決められた手順に従って処理し、その結果を返すものである。「Rで起こるあらゆることは関数の呼び出しである (Everything that happens in R is a function call)」(Chambers 2016)。
関数は 関数名(関数の入力となるオブジェクト) のように使う。たとえば、class(my_vec1)はmy_vec1というオブジェクトのクラスを返す関数である。また、sum(my_vec1)はmy_vec1の要素の総和を計算して返す関数である。
関数は自分で作成することもできる(次章で説明する)。複雑な作業を繰り返す場合、その作業を関数として記述することで、一行でその作業を再現することが可能になる。「2度以上同じことを繰り返すなら関数を作れ」というのが、Rユーザの心得である。
関数を使うためには、 引数 (ひきすう) と呼ばれるものが必要である。例えば、sum() 関数はこれだけだと何もできない。何らかのオブジェクトがが入力として与えられないと、結果を返すことができない。sum(my_vec1)のようにすることではじめて結果が返される。ここでmy_vec1がsum()関数の引数である。
関数は引数は複数持つことができる。たとえば、欠測値を含む以下のmy_vec2を考えてみよう。
この数列の総和を sum()関数で求めようとすると、結果は欠測値 NA になる。
これはsum()の基本仕様が「入力に欠測値が含まれている場合は欠測値を返す」ことになっているためである。入力されたデータのなかで欠測値を除いたものの総和を求めるために、sum()はもう1つの引数が用意されている。それが na.rm である。na.rm = TRUEを指定すると、欠測値を除外した総和を返す。
第7章で説明したとおり、na.rm などのように引数を区別するために関数によって用意されたものを仮引数 (parameter) と呼び、TRUE のように引数の中身としてユーザが指定するものを実引数 (argument) と呼ぶ。my_vec2 は実引数である。
引数を指定するとき、my_vec2のように仮引数を明示しないこともある。多くの場合、それがなければ関数がそもそも動かないという第1引数の仮引数は明示しないことが多い。そもそも、第1引数には仮引数がない(名前がない)場合もある。sum() 関数の第1引数はnumeric または complex 型のベクトルだが、仮引数がそもそもない(... で定義されている)。
しかし、第1引数以外については、na.rm = TRUE のように仮引数を明示すべきである。関数によっては数十個の引数をとるものもあり、仮引数を明示しないと、どの実引数がどの仮引数に対応するのかわかりにくい。
多くの引数は関数によって定義された既定値 (default value) をもっている。たとえば、sum()関数の na.rm の既定値は FALSE である。既定値が用意されている場合、その引数を指定せずに関数を使うことができる。
ある関数がどのような引数を要求しているか、その既定値は何か、引数として何か決められたデータ型/データ構造があるかを調べたいときは ?関数名 (()がないことに注意)または help(関数名)をコンソールに入力する。多くの関数に詳細なヘルプが付いているので、ネットで検索したり、誰かに質問する前にひとまずヘルプを読むべきである。
11.2 Rのコーディングスタイル
Rコードの書き方に唯一の正解はない。文法が正しければ、つまり、Rが意図どおりの実行結果を出してくれさえすれば、それは「正しい」コードである。しかし、コードというのは、一度書けば二度と読まないというものではない。最初に「書く」とき以外に、「修正する」ときや「再利用」するときなどに繰り返し読むことになる。また、共同研究をする場合には共同研究者がコードを読む。さらに、近年では研究成果を報告する際に分析に利用したコードを後悔公開することも当たり前になりつつあり、その場合には自分で書いたコードを世界中の人が読む可能性がある。
ここで大事なのは、Rコードを読むのはRだけではないということである。人間も重要な「読者」である。Rコードを書くときは人間に優しいコードを書くように心がえよう。唯一の正解がなくても、読みやすく、多くの人が採用している標準的な書き方はある。ここでは、人間に優しいコードを書くためのコーディングスタイルについて説明しよう。
11.2.1 オブジェクト名
ベクトル、データフレーム、自作の関数などのすべてのオブジェクトには名前をつける必要がある(ラムダ式などの無名関数を除く)。名前はある程度自由に付けることができるが、大事な原則がある。それは、
- オブジェクト名は英数字と限られた記号のみにする
- 数字で始まる変数名は避ける
1つ目は原則にすぎない。よって、日本語やハングルの変数名をつけることもできる。しかし、それは推奨しない。英数字以外(マルチバイト文字)は文字化けの可能性がある(文字コードの問題が発生する)し、コードを書く際列がずれる原因にもなる
2つ目のルールは必ず守る必要がある。つまり、数字で始まるオブジェクト名は作成できない。
予約語を避ける
Rが提供する組込の関数やオブジェクトと重複する名前を自分で作成するオブジェクトに付けるのは避けよう。例えば、Rには円周率 (\(\pi\)) がpiという名前で用意されている。
piという名前で新しい変数を作ることはできる。
しかし、既存のpiが上書きされてしまうので避けたほうが良い。
元の円周率を使うこともできるが、手間が増える。
また、ユーザが自由に使えない名前もある。それらの名前を「予約語」と呼ぶ。予約語の使用は禁止されている。
このように、予約語はそもそも使えないので、それほど意識する必要はない。
ただし、以下のコードのようなことが起こるので注意してほしい。
ここから、T は TRUE、F は FALSE と同じ働きをしていることがわかる。ここで、次のコードを実行してみよう。
このように、T とFはあらかじめ使える状態で用意されているものの、予約語ではないので値が代入できてしまう。しかし、T とF を自分で定義したオブジェクトの名前に使うと混乱の元になるので、使用は避けるのが無難である。
また、TRUE と FALSE を T やF で済ませる悪習は廃して常に完全にスペルすべきである。
「短さ」と「分かりやすさ」を重視する
オブジェクト名を見るだけでその中にどのようなデータが含まれているか推測できる名前をつけるべきである。たとえば、性別を表す変数を作るなら、
ではなく、
としたほうが良い。gender という名前がついていれば、「この変数には性別に関する情報が入っているだろう」と容易に想像できる。
また、オブジェクト名は、短くて読みやすいほうが良い。数学の成績データをもつ次のオブジェクトについて考えよう。
オブジェクト名から、中にどのような情報が含まれるか想像することはできる。しかし、この名前は長く、読みにくい。そこで、次のように名前を変えたほうが良い。
これらの例では、mathematics を math に縮め、mathとscoreの間に区切りを入れて読みやすくしている。大文字と小文字の組み合わせで区切る方法はキャメルケース (camel case)と呼ばれ、大文字から始まるキャメルケースを大文字キャメルケース (upper camel case)、小文字から始まるキャメルケースを小文字キャメルケース (lower camel case) と呼ぶ。また、_(アンダーバー, アンスコ) で区切る方法はスネークケース (snake case) と呼ばれる。キャメルケースとスネークケースは、一貫した方法で使えばどちらを使っても良いだろう。
かつては “.” を使って単語を繋ぐのが標準的だった時代もあり、組込関数には “.” を使ったものも多い。data.frame() や read.csv() などがその例である。しかし、“.” はクラスのメソッドとして使われることがあり、混乱するので使うのは避けたほうが良い。{tidyverse} では、readr::read_csv() のように、スネークケースが採用されている。他にも -(ハイフン)で区切るチェーンケース (chain case)というのもあるが、Rで-は「マイナス(減算演算子)」であり、チェーンケースは使えない7。
11.2.2 改行
コードは1行が長すぎないように適宜改行する。Rやパッケージなどが提供している関数のなかには10個以上の引数を必要とするものもあり。コードを1行で書こうとするとコードの可読性が著しく低くなってしまう。
1行に何文字入れるべきかについて決まったルールはないが、1行の最大文字数を半角80字にするという基準が伝統的に使われてきた。これま昔のパソコンで使ったパンチカード ( 図 11.2 )では1行に80個の穴を開けることができたことに由来する。
最近は昔と比べてモニタのサイズが大きく、解像度も高いので、80文字にこだわる必要はない。自分のRStudioのSource Paneに収まるよう、切りがいいところで改行しよう。
11.2.3 スペースとインデント
適切なスペースはコードの可読性を向上させる。以下の2つのコードは同じ内容を実行するが、後者にはスペースがないので読にくい。
どこにスペースを入れるかについてのルールは特になり。Rは「半角スペース」を無視するので、プログラムの動作に関して言えば、半角スペースはあってもなくても同じである。標準的なルールとして、「,の後にスペース」、「演算子の前後にスペース」などが考えらえる。ただし、^の前後にはスペースを入れないことが多い。また、後ほど紹介するfor(){}、while(){}、if(){}などのように、関数以外の ()の前後にはスペースを入れる。それに対し、sum() や read.csv() などのように、関数のかっこの場合には、関数名と()の間にスペースを入れない。
また、スペースを2回以上入れることもある。たとえば、あるデータフレームを作る例を考えよう。
上の2つのコードの内容は同じだが、前者のほうが読みやすい。
ここでもう1つ注目してほしいのは、「字下げ (indent)」の使い方である。上のコードは次のように一行にまとめることができるが、読みにくい。
このように1行のコードが長い場合、「改行」が重要である。しかし、Rの最も基本的なルールは「1行に1つのコード」なので、改行するとこのルールを破ることになってしまう。そこで、コードの途中で改行するときは、2行目以降を字下げすることで、1つのコードが前の行から続いていることを明確にしよう。前の行の続きの行は2文字(または4文字)分字下げする。こうすることで、「この行は上の行の続き」ということが「見て」わかるようになる。
RStudioは自動的に字下げをしてくれるので、RStudioを使っているならあまり意識する必要はない。自動で字下げしてくれないエディタを使っている場合は、字下げに気をつけよう。
11.2.4 代入
オブジェクトに値を代入する演算子として、これまで <- を使ってきたが、=を使うこともできる。R以外の多くのプログラミング言語では、代入演算子として=を採用している。しかし、引数の指定と代入を区別するためん、本書では<-の使用を推奨する。また、既に説明したとおり、option + -(macOSの場合)または Alt + -(Windows の場合)というショートカットを使うと、<- だけでなく、その前後のスペースを自動的に挿入してくれる。コードが読みやすくなるので、代入演算子は常にショートカットで入力する習慣をつけよう。
この節で説明したコードの書き方は、コーディングスタイルの一部に過ぎない。本書では、できるだけ読みやすいコードを例として示すことを心がけている。最初は本書(あるいは他の本やウェブサイトに掲載されているもののなかで気に入ったもの)のスタイルを真似して書いてみてほしい。コードを書き写しているうちに、自にとって最善の書き方が見えてくるだろう。
コーディングスタイルについては、以下の2つの資料がさらに詳しい。とりわけ、羽鳥先生が書いたThe tidyverse style guide は事実上の業界標準であり、Google’s Style Guide も このガイドをベースにしている。かなりの分量だが、パッケージ開発などを考えているなら一度は目を通しておいたほうが良いだろう。
11.3 反復
人間があまり得意ではないが、コンピュータが得意とする代表的な作業が「反復作業」である。普通の人間なら数時間から数年かかるような退屈な反復作業でも、コンピュータを使えば数秒で終わることが多い。Rでもさまざまな反復作業を行うことができる。
反復作業を行うときは、反復作業を「いつ終わらせるか」を考えなくてはならない。終わりを規程する方法として、以下の2つが考えられる。
- 処理を繰り返し回数を指定し、その回数に達したら終了する:
for文を使う - 一定の条件を指定し、その条件が満たされるときに処理を終了する:
while文を使う
この節では、これら2つのケースのそれぞれについて解説する。
11.3.1 for による反復
forを利用した反復を、forループ と呼ぶ。まず、forループの雛形をみてみよう。
任意の変数の選び方はいろいろ考えられるが、よく使うのはインデクス (index) i である。このインデクスはforループの内部で使うために用いられる変数である。そして、in の後の「ベクトル」には長さ1以上のベクトルを指定する。ベクトルは必ずしもnumeric型である必要はないが、インデクスを利用したforループではインデクスを指定する正の整数を要素にもつベクトルを使う。
また、{}内の内容が1行のみの場合には、{}は省略しても良い。その場合、処理内容を()の直後に書。つまり、以下のような書き方もできる。
これはforだけでなく、ifやfunction()など、{}で処理内容を囲む関数に共通である。処理内容が2行以上の場合は、必ず{}で囲むようにしよう。
例として、インデクス \(i\) の内容を画面に表示することをN回繰り返すforループを書いてみよう。繰り返し回数を指定するために、for (i in 1:N)と書く。5回繰り返すならfor (i in 1:5)とする。1:5はc(1, 2, 3, 4, 5)と同じなので、for (i in c(1, 2, 3, 4, 5))でもいいが、1:5 を使って書いたほうが、コードが読みやすい。
以下コードを作成し、実行してみよう。このコードは3行がひとつのかたまりになったコードである。forの行(あるいは最後の}の行)にカーソルを置いた状態で Cmd/Ctrl + Return/Enter を押せば、forループ全体が一挙に実行される。
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
1から5までの数字が表示された。
ここで、このforループがどのような順番で処理を実行したのか確認してみよう。上のコードは、次のようなステップを踏んで1から5までの数字を画面に表示している。
iにベクトル1:5の最初の要素を代入 (i <- 1)print(i)を実行{}中身の処理が終わったらiにベクトル1:5内の次の要素を代入 (i <- 2)print(i)を実行{}中身の処理が終わったらiにベクトル1:5内の次の要素を代入 (i <- 3)print(i)を実行{}中身の処理が終わったらiにベクトル1:5内の次の要素を代入 (i <- 4)print(i)を実行{}中身の処理が終わったらiにベクトル1:5内の次の要素を代入 (i <- 5)print(i)を実行{}中身の処理が終わったらiにベクトル1:5内の次の要素を代入するが、5が最後の要素なので反復終了
この手順を要約すると、次のようになる。
- 任意の変数 (ここでは
i)にベクトル (ここでは1:5)の最初の要素が格納され、{}内の処理を行う。 {}内の処理が終わったら、ベクトル (ここでは1:5)の次の要素を任意の変数 (ここではi)に格納し、{}内の処理を行う。- 格納できる要素がなくなったら反復を終了する。
- したがって、反復はベクトル (ここでは
1:5)の長さだけ実行される (ここでは5回)。
- したがって、反復はベクトル (ここでは
反復回数はベクトルの長さで決まる。既に述べたとおり、1:5のような書き方でなく、普通のベクトルを指定しても問題ない。たとえば、長さ6の dmg_vals というベクトルを作り、その要素を出力するコードを書いてみよう。
dmg_vals <- c(24, 64, 31, 46, 81, 102)
for (damage in dmg_vals) {
x <- paste0("トンヌラに", damage, "のダメージ!!")
print(x)
}[1] "トンヌラに24のダメージ!!"
[1] "トンヌラに64のダメージ!!"
[1] "トンヌラに31のダメージ!!"
[1] "トンヌラに46のダメージ!!"
[1] "トンヌラに81のダメージ!!"
[1] "トンヌラに102のダメージ!!"
スライムくらいなら一撃で撃破できそうな立派な勇者である。
ちなみに、paste0()は引数を空白なし8で繋いだ文字列を返す関数である。詳細は第18で解説するが、簡単な例だけ示しておこう。
[1] "Rはみんなのともだち"
[1] "私のHP/MPは500/400です。"
文字列のベクトルを使ったforループもできる。10個の都市名が格納されたcitiesの要素を1つずつ出力するコードは次のように書ける。
cities <- c("Sapporo", "Sendai", "Tokyo", "Yokohama", "Nagoya",
"Kyoto", "Osaka", "Kobe", "Hiroshima", "Fukuoka")
for (i in seq_along(cities)) {
x <- paste0("現在、cityの値は", cities[i], "です。")
print(x)
}[1] "現在、cityの値はSapporoです。"
[1] "現在、cityの値はSendaiです。"
[1] "現在、cityの値はTokyoです。"
[1] "現在、cityの値はYokohamaです。"
[1] "現在、cityの値はNagoyaです。"
[1] "現在、cityの値はKyotoです。"
[1] "現在、cityの値はOsakaです。"
[1] "現在、cityの値はKobeです。"
[1] "現在、cityの値はHiroshimaです。"
[1] "現在、cityの値はFukuokaです。"
ここでは for (i in 1:10) ではなく、for (i in seq_along(cities))と表記。この例からわかるように、seq_along() を使うと、ベクトルのインデクス自動的に作ってくれる。上の例では、seq_along(ten_cities) が自動的にベクトル長さを計算し、1:length(ten_cities) すなわち 1:10 と同じ処理をしてくれる。ベクトルの長さが自明でないとき(ベクトルが非常に長いとき)には、seq_along() を使うのが便利である。後で cities の中身を書き換えたときに、cities の長さが変わることも考えられるので、ベクトルのインデクス指定には seq_along() を使おう。
また、インデクスを使わないforループも書ける。上と同じ結果は、次のコードで実現することができる。
[1] "現在、cityの値はSapporoです。"
[1] "現在、cityの値はSendaiです。"
[1] "現在、cityの値はTokyoです。"
[1] "現在、cityの値はYokohamaです。"
[1] "現在、cityの値はNagoyaです。"
[1] "現在、cityの値はKyotoです。"
[1] "現在、cityの値はOsakaです。"
[1] "現在、cityの値はKobeです。"
[1] "現在、cityの値はHiroshimaです。"
[1] "現在、cityの値はFukuokaです。"
このように、ベクトルの中身が数字でない場合でも、forループでベクトルの要素を1つずつ順番に利用することができる。
次に、"1番目の都市名はSapporoです"、"2番目の都市名はSendaiです"、… のように出力する方法を考えよう。これまでは出力する文字列のなかで1箇所(都市名)のみを変えながら表示したが、今回は「i」と「city」の2箇所を同時に変える。これは、インデクス iを使って反復を実行しながら、cities のi番目要素を呼び出すことで実現できる。以下のコードを実行してみよう。
[1] "1番目の都市名はSapporoです。"
[1] "2番目の都市名はSendaiです。"
[1] "3番目の都市名はTokyoです。"
[1] "4番目の都市名はYokohamaです。"
[1] "5番目の都市名はNagoyaです。"
[1] "6番目の都市名はKyotoです。"
[1] "7番目の都市名はOsakaです。"
[1] "8番目の都市名はKobeです。"
[1] "9番目の都市名はHiroshimaです。"
[1] "10番目の都市名はFukuokaです。"
このように、インデクスとそれに対応するベクトルの要素を抽出すれば、望みどおりの処理ができる。
多重forループ
forループの中でさらにforループを使うともできる。最初はやや難しいかもしれないが、多重反復はプログラミング技術としてよく使われるので9、この機会に勉強しよう。
多重forループを理解するためにうってつけの例は掛け算九九である。積算演算子 * の「左の数」と「右の数」 のそれぞれに1から9までの数字を代入するすべての組み合わせを考えるためには、2つのforループが必要だ10。i * jでiとjそれぞれに1から9を代入しながら結果を出力するコードは次のように書ける。
[1] "1 * 1 = 1"
[1] "1 * 2 = 2"
[1] "1 * 3 = 3"
[1] "1 * 4 = 4"
[1] "1 * 5 = 5"
[1] "1 * 6 = 6"
[1] "1 * 7 = 7"
[1] "1 * 8 = 8"
[1] "1 * 9 = 9"
[1] "2 * 1 = 2"
[1] "2 * 2 = 4"
[1] "2 * 3 = 6"
[1] "2 * 4 = 8"
[1] "2 * 5 = 10"
[1] "2 * 6 = 12"
[1] "2 * 7 = 14"
[1] "2 * 8 = 16"
[1] "2 * 9 = 18"
[1] "3 * 1 = 3"
[1] "3 * 2 = 6"
[1] "3 * 3 = 9"
[1] "3 * 4 = 12"
[1] "3 * 5 = 15"
[1] "3 * 6 = 18"
[1] "3 * 7 = 21"
[1] "3 * 8 = 24"
[1] "3 * 9 = 27"
[1] "4 * 1 = 4"
[1] "4 * 2 = 8"
[1] "4 * 3 = 12"
[1] "4 * 4 = 16"
[1] "4 * 5 = 20"
[1] "4 * 6 = 24"
[1] "4 * 7 = 28"
[1] "4 * 8 = 32"
[1] "4 * 9 = 36"
[1] "5 * 1 = 5"
[1] "5 * 2 = 10"
[1] "5 * 3 = 15"
[1] "5 * 4 = 20"
[1] "5 * 5 = 25"
[1] "5 * 6 = 30"
[1] "5 * 7 = 35"
[1] "5 * 8 = 40"
[1] "5 * 9 = 45"
[1] "6 * 1 = 6"
[1] "6 * 2 = 12"
[1] "6 * 3 = 18"
[1] "6 * 4 = 24"
[1] "6 * 5 = 30"
[1] "6 * 6 = 36"
[1] "6 * 7 = 42"
[1] "6 * 8 = 48"
[1] "6 * 9 = 54"
[1] "7 * 1 = 7"
[1] "7 * 2 = 14"
[1] "7 * 3 = 21"
[1] "7 * 4 = 28"
[1] "7 * 5 = 35"
[1] "7 * 6 = 42"
[1] "7 * 7 = 49"
[1] "7 * 8 = 56"
[1] "7 * 9 = 63"
[1] "8 * 1 = 8"
[1] "8 * 2 = 16"
[1] "8 * 3 = 24"
[1] "8 * 4 = 32"
[1] "8 * 5 = 40"
[1] "8 * 6 = 48"
[1] "8 * 7 = 56"
[1] "8 * 8 = 64"
[1] "8 * 9 = 72"
[1] "9 * 1 = 9"
[1] "9 * 2 = 18"
[1] "9 * 3 = 27"
[1] "9 * 4 = 36"
[1] "9 * 5 = 45"
[1] "9 * 6 = 54"
[1] "9 * 7 = 63"
[1] "9 * 8 = 72"
[1] "9 * 9 = 81"
上のコードがどのような処理をしているか考えてみよう。まず、iに1が代入される。次に、jに1から9までの数順番に1つずつ代入され、1 * 1、1 * 2、1 * 3、…、1 * 9が計算される。1 * 9の計算が終わったら、iに2が代入される。そして再び内側のforループが実行され、2 * 1、2 * 2、2 * 3、…、2 * 9が計算される。これを9の段まで繰り返す。段の順番を降順にして9の段を最初に計算し、1の段を最後に計算したい場合は、i in 1:9をi in 9:1に変えればよい。
[1] "9 * 1 = 9"
[1] "9 * 2 = 18"
[1] "9 * 3 = 27"
[1] "9 * 4 = 36"
[1] "9 * 5 = 45"
[1] "9 * 6 = 54"
[1] "9 * 7 = 63"
[1] "9 * 8 = 72"
[1] "9 * 9 = 81"
[1] "8 * 1 = 8"
[1] "8 * 2 = 16"
[1] "8 * 3 = 24"
[1] "8 * 4 = 32"
[1] "8 * 5 = 40"
[1] "8 * 6 = 48"
[1] "8 * 7 = 56"
[1] "8 * 8 = 64"
[1] "8 * 9 = 72"
[1] "7 * 1 = 7"
[1] "7 * 2 = 14"
[1] "7 * 3 = 21"
[1] "7 * 4 = 28"
[1] "7 * 5 = 35"
[1] "7 * 6 = 42"
[1] "7 * 7 = 49"
[1] "7 * 8 = 56"
[1] "7 * 9 = 63"
[1] "6 * 1 = 6"
[1] "6 * 2 = 12"
[1] "6 * 3 = 18"
[1] "6 * 4 = 24"
[1] "6 * 5 = 30"
[1] "6 * 6 = 36"
[1] "6 * 7 = 42"
[1] "6 * 8 = 48"
[1] "6 * 9 = 54"
[1] "5 * 1 = 5"
[1] "5 * 2 = 10"
[1] "5 * 3 = 15"
[1] "5 * 4 = 20"
[1] "5 * 5 = 25"
[1] "5 * 6 = 30"
[1] "5 * 7 = 35"
[1] "5 * 8 = 40"
[1] "5 * 9 = 45"
[1] "4 * 1 = 4"
[1] "4 * 2 = 8"
[1] "4 * 3 = 12"
[1] "4 * 4 = 16"
[1] "4 * 5 = 20"
[1] "4 * 6 = 24"
[1] "4 * 7 = 28"
[1] "4 * 8 = 32"
[1] "4 * 9 = 36"
[1] "3 * 1 = 3"
[1] "3 * 2 = 6"
[1] "3 * 3 = 9"
[1] "3 * 4 = 12"
[1] "3 * 5 = 15"
[1] "3 * 6 = 18"
[1] "3 * 7 = 21"
[1] "3 * 8 = 24"
[1] "3 * 9 = 27"
[1] "2 * 1 = 2"
[1] "2 * 2 = 4"
[1] "2 * 3 = 6"
[1] "2 * 4 = 8"
[1] "2 * 5 = 10"
[1] "2 * 6 = 12"
[1] "2 * 7 = 14"
[1] "2 * 8 = 16"
[1] "2 * 9 = 18"
[1] "1 * 1 = 1"
[1] "1 * 2 = 2"
[1] "1 * 3 = 3"
[1] "1 * 4 = 4"
[1] "1 * 5 = 5"
[1] "1 * 6 = 6"
[1] "1 * 7 = 7"
[1] "1 * 8 = 8"
[1] "1 * 9 = 9"
九九を覚えるときにはこれで良いが、実数どうしの掛け算で i * jとj * i は同じ(交換法則が成り立つ)なので、2つの数字の積を知りたいだけなら片方だけ出力すれば十分だろう。そこで、for (j in 1:9) を for (j in i:9) に変えてみよう。
[1] "1 * 1 = 1"
[1] "1 * 2 = 2"
[1] "1 * 3 = 3"
[1] "1 * 4 = 4"
[1] "1 * 5 = 5"
[1] "1 * 6 = 6"
[1] "1 * 7 = 7"
[1] "1 * 8 = 8"
[1] "1 * 9 = 9"
[1] "2 * 2 = 4"
[1] "2 * 3 = 6"
[1] "2 * 4 = 8"
[1] "2 * 5 = 10"
[1] "2 * 6 = 12"
[1] "2 * 7 = 14"
[1] "2 * 8 = 16"
[1] "2 * 9 = 18"
[1] "3 * 3 = 9"
[1] "3 * 4 = 12"
[1] "3 * 5 = 15"
[1] "3 * 6 = 18"
[1] "3 * 7 = 21"
[1] "3 * 8 = 24"
[1] "3 * 9 = 27"
[1] "4 * 4 = 16"
[1] "4 * 5 = 20"
[1] "4 * 6 = 24"
[1] "4 * 7 = 28"
[1] "4 * 8 = 32"
[1] "4 * 9 = 36"
[1] "5 * 5 = 25"
[1] "5 * 6 = 30"
[1] "5 * 7 = 35"
[1] "5 * 8 = 40"
[1] "5 * 9 = 45"
[1] "6 * 6 = 36"
[1] "6 * 7 = 42"
[1] "6 * 8 = 48"
[1] "6 * 9 = 54"
[1] "7 * 7 = 49"
[1] "7 * 8 = 56"
[1] "7 * 9 = 63"
[1] "8 * 8 = 64"
[1] "8 * 9 = 72"
[1] "9 * 9 = 81"
このコードは、1の段は1 * 1から1 * 9 までのすべてを計算するが、2の段は2 * 2から、3の段は3 * 3から計算を始めて出力するコードである。2の段の場合、2 * 1は1の段で 1 * 2 が計算済みなのでスキップする。3の段の場合、3 * 1 は1の段で、3 * 2 は2の段でそれぞれ計算済みなのでとばす。 それぞれの段で同様の処理を続け、最後の9の段では9 * 9 のみが計算される。
このように多重forループは複数のベクトルの組み合わせて処理を行う場合に便利である。
複数のベクトルでなく、データフレームなどの2次元以上データにも多重forループは使われる。データフレームは複数のベクトルで構成されている(各列が1つのベクトル)ため、実質的には複数のベクトルを扱うことになる。
例として、FIFA_Men.csv を利用し、それぞれの国のチーム名、FAFAランキング、ポイントをまとめて表示するコードを書いてみよう。すべてのチームを表示すると結果が長くなるので、対象をOFC (オセアニアサッカー連盟) 所属チームに限定する。
# read_csv()を使用するために{tidyverse}を読み込む
pacman::p_load(tidyverse)
# FIFA_Men.csvを読み込み、myDFという名で保存
my_df <- read_csv("Data/FIFA_Men.csv")
# my_dfのConfederation列がOFCの行だけを抽出
my_df <- my_df[my_df$Confederation == "OFC", ]
my_df# A tibble: 10 × 6
ID Team Rank Points Prev_Points Confederation
<dbl> <chr> <dbl> <dbl> <dbl> <chr>
1 4 American Samoa 192 900 900 OFC
2 69 Fiji 163 996 996 OFC
3 135 New Caledonia 156 1035 1035 OFC
4 136 New Zealand 122 1149 1149 OFC
5 147 Papua New Guinea 165 991 991 OFC
6 159 Samoa 194 894 894 OFC
7 171 Solomon Islands 141 1073 1073 OFC
8 185 Tahiti 161 1014 1014 OFC
9 191 Tonga 203 862 862 OFC
10 204 Vanuatu 163 996 996 OFC
OFCに10チーム加盟していることがわかる。
以下のような内容が表示されるコードを書きたい。
=====1番目のチーム情報=====
Team: American Samoa
Rank: 192
Points: 900
=====2番目のチーム情報=====
Team: Fiji
Rank: 163
Points: 996
=====3番目のチーム情報=====
Team: New Caledonia
...
これは1つのforループでも作成できるが、勉強のために2つのforループを使って書いてみよう。
以上のコードを実行すると以下のような結果が表示される(全部掲載すると長くなるので、ここでは最初の2チームのみ掲載する。
[1] "=====1番目のチーム情報====="
[1] "Team: American Samoa"
[1] "Rank: 192"
[1] "Points: 900"
[1] "=====2番目のチーム情報====="
[1] "Team: Fiji"
[1] "Rank: 163"
[1] "Points: 996"
このコードについて説明しよう。まず、外側のforループでは任意の変数としてインデクスiを、内側のforループではインデクスjを使っている。Rは、コードを上から順番に処理する(同じ行なら、カッコの中から処理する)。したがって、まず処理されるのは外側のforループである。iにはベクトル1:nrow(my_df)の要素が順番に割り当てられる。nrow() は行列またはデータフレームの行数を求める関数で、my_dfは10行のデータなので1:10になる。つまり、外側のforループはiに1, 2, 3, …, 10の順で値を格納しながらループ内の処理を繰り返す。ここで、外側のforループの中身を見てみよう。まず、print() を使って "=====i番目のチーム情報=====" というメッセージを出力している。最初はiが1なので、"=====1番目のチーム情報====="が表示される。
次に実行されるのが内側のforループである。ここではjにc("Team", "Rank", "Points")を格納しながら内側のforループの内のコードをが3回繰り返し処理される。内側のコードの内容は、たとえば、i = 1の状態で、j = "Team"なら、print(paste0("Team", ": ", my_df[1, "Team"]))である。第10.4章で説明した通り、my_df[1, "Team"]はmy_dfのTeam 列の1番目の要素を意味する。この処理が終わると、次はjに"Rank"が代入され、同じコードを処理する。そして、j = "Points"まで処理が終わったら、内側のforループは一度終了する。
内側のforループが終わっても、外側のforループはまだ終わっていない。次は、i に 2 が格納され、"=====2番目のチーム情報=====" を表示し、再度内側のforループを最初から処理する。 i = 10の状態で内側のforループが終了ると外側のforループも終了する。多重forループはこのように反復処理を実行する。
チーム名の次に所属連盟も表示したいときはどう直せば良いだろうか。正解はc("Team", "Rank", "Points")のベクトルで、"Team"と"Rank"の間に"Confederation"を追加するだけである。実際にやってみよう (スペースの関係上、最初の2チームの結果のみ掲載する)。
[1] "=====1番目のチーム情報====="
[1] "Team: American Samoa"
[1] "Confederation: OFC"
[1] "Rank: 192"
[1] "Points: 900"
[1] "=====2番目のチーム情報====="
[1] "Team: Fiji"
[1] "Confederation: OFC"
[1] "Rank: 163"
[1] "Points: 996"
ちなみに、1つのforループで同じ結果を実現するには次のようにする。結果は掲載しないが、自分で試してみてほしい。また、cat()関数の使い方については?catを参照されたい。ちなみに\nは改行コードである。
リスト型を対象とした多重forループの例も確認しておこう。複数のベクトルを含むリストの場合、3番目のベクトルの5番目の要素を抽出するには、リスト名[[3]][5] のように2つの位置を指定する必要がある。たとえば、3人で構成されたグループが3つあり、それぞれのグループにおける id (例:学籍番号)の順番で名前が格納されている my_listを考えてみよう。
ここで、まず各クラスの出席番号1番の人の名字をすべて出力し、次は2番の人、最後に3番の人を表示するにはどうすれば良いだろうか。以下のコードを見てみよう。
for (i in 1:3) {
for (j in names(my_list)) {
print(my_list[[j]][i])
}
print(paste0("===ここまでが出席番号", i, "番の人です==="))
}[1] "Song"
[1] "Watanabe"
[1] "Abe"
[1] "===ここまでが出席番号1番の人です==="
[1] "Wickham"
[1] "Toyoshima"
[1] "Moon"
[1] "===ここまでが出席番号2番の人です==="
[1] "Yanai"
[1] "Fujii"
[1] "Xi"
[1] "===ここまでが出席番号3番の人です==="
このコードは以下のように動く。
- 1行目: 任意の変数を
iとし、1から3までの数字をiに格納しながら、3回反復作業を行う。 - 3行目: 任意の変数を
jとし、ここにはリストの要素名(A, B, C)を格納しながら、リストの長さだけ処理を繰り返す。names(my_list)はc("A", "B", "C")である。 - 4行目:
my_list[[j]][i]の内容を出力する。最初はi = 1、j = "A"なので、print(my_list[["A"]][1])、つまり、my_listから"A"を取り出し、そこの1番目の要素を出力する - 7行目: 各ベクトルから
i番目の要素を出力し、print(paste0("===ここまでが出席番号", i, "番の人です==="))を実行する。iに次の要素を入れ、作業を反復する。iに格納する要素がなくなったら、反復を終了する。
多重forループは3重、4重にすることも可能だが、コードの可読性が低下するため、多くても3重、できれば最大二重までにしておいたほうがよい。3重以上にforループを重ねる場合は、内側のforループを後ほど解説する関数でまとめたほうがいいだろう。
上の例では、リストの各要素に名前 (A, B, C) が付けられていることを利用した。リストに名前がない場合はどうすれば良いだろうか(そもそも、名前のないリストなど作らないほうが良いのだが…)。例えば、次のような場合である。
この場合には、次のようにすればよい。
for (i in 1:3) {
for (j in seq_along(my_list)) {
print(my_list[[j]][i])
}
print(paste0("===ここまでが出席番号", i, "番の人です==="))
}[1] "Song"
[1] "Watanabe"
[1] "Abe"
[1] "===ここまでが出席番号1番の人です==="
[1] "Wickham"
[1] "Toyoshima"
[1] "Moon"
[1] "===ここまでが出席番号2番の人です==="
[1] "Yanai"
[1] "Fujii"
[1] "Xi"
[1] "===ここまでが出席番号3番の人です==="
内側のforループでリストの要素名を使う代わりに、リストの要素を位置で指定している。
11.3.2 whileによる反復
forによるループは任意の変数にベクトルの要素を1つずつ代入し、ベクトルの要素を使い尽くすまで反復処理を行う。したがって、ベクトルの長さによってループの回数が決まる。
それに対し、「ある条件が満たされる限り、反復し続ける」あるいは「ある条件が満たされるまで、反復し続ける」ことも可能である。そのときに使うのが、whileによる whileループである。whileループの書き方はforループにとてもよく似ている。
基本的な使い方は、以下のとおりである。
forがwhileに変わり、()内の書き方が(任意の変数 in ベクトル)から(条件)に変わった。この()内の条件が満たされる間は{}内の内容を処理が繰り返し実行される。1から5までの整数を表示するコードは、forループを使うと次のように書ける。
これをwhileループを使って書き直すと、次のようになる。
whileループを繰り返す条件はi < 6である。これにより、「iが6未満」なら処理が繰り返される。注意すべき点として、i が6未満か否かの判断をループの開始時点で行うので、あらかじめ変数 i を用意する必要があることがあげられる。1行めにある i <- 1 がそれである。そして、{}内の最終行に i <- i + 1 があり、iを1ずつ増やしている。i = 5 の時点では print(i) が実行されるが、i = 6になると、ループをもう1ど実行する条件が満たされないので、反復が停止する。
i <- i + 1のように内容を少しずつ書き換えるさいは、そのコードを置く位置にも注意が必要だ。先ほどのコードのうち、i <- i + 1 を print(i)の前に移動してみよう。
2から6までの整数が出力された。これはiを出力する前にiに1が足されるためである。i <- i + 1の位置をこのままにして、先ほどと同じように1から5までの整数を表示するには、次のようにコードを書き換える。
変更点したのは、
1.iの初期値を0にした 2. ()内の条件を(i < 6)から(i < 5)にした
という2点である。
whileループは慣れないとややこしいと感じるだろう。forループで代替できるケースも多い。それでもwhile文が必要になるケースもある。それは先述した通り、「目標は決まっているが、その目標が達成されるまでは処理を何回繰り返せばいいか分からない」場合である。
たとえば、6面のサイコロ投げを考えよう。投げる度に出た目を記録し、その和が30以上に達したときサイコロ投げを中止するにはどうすればいいだろうか。何回サイコロを投げればいいかがあらかじめわからないと、forループは使えない。連続で6が出るなら5回で十分かもしれないが、1が出続けるなら30回投げる必要がある。このように、「反復処理は行うが、何回行えばいいか分からない。ただし、停止条件は知っている」場合にwhileループを使う。
サイコロ投げの例は、以下のコードで実行できる。
total <- 0
trial <- 1
while (total < 30) {
die <- sample(1:6, size = 1)
total <- total + die
print(paste0(trial, "回目のサイコロ投げの結果: ", die,
"(これまでの総和: ", total, ")"))
# print()文は以下のような書き方も可能
# Result <- sprintf("%d回目のサイコロ投げの結果: %d (これまでの総和: %d)",
# trial, die, total)
# print(Result)
trial <- trial + 1
}[1] "1回目のサイコロ投げの結果: 1(これまでの総和: 1)"
[1] "2回目のサイコロ投げの結果: 5(これまでの総和: 6)"
[1] "3回目のサイコロ投げの結果: 6(これまでの総和: 12)"
[1] "4回目のサイコロ投げの結果: 3(これまでの総和: 15)"
[1] "5回目のサイコロ投げの結果: 3(これまでの総和: 18)"
[1] "6回目のサイコロ投げの結果: 5(これまでの総和: 23)"
[1] "7回目のサイコロ投げの結果: 3(これまでの総和: 26)"
[1] "8回目のサイコロ投げの結果: 4(これまでの総和: 30)"
このコードの中身を説明しよう。まず、これまで出た目の和を記録する変数totalを用意し、初期値として0を格納する。また、何回目のサイコロ投げかを記録するためにtrialという変数を用意し、初期値として1を格納する(コードの1、2行目)。
次に、while文を書く。反復する条件はtotalが30未満と設定する。つまり、totalが30以上になったら反復を終了する(コードの4行目)。
続いて、サイコロを投げ、出た目をdieという変数に格納します。サイコロ投げは1から6の間の整数から無作為に1つを値を抽出することでシミュレートできる。そこで使われるのが sample()関数である(コードの5行目)。sample() 関数は与えられたベクトル内の要素を無作為に抽出する。sample(c(1, 2, 3, 4, 5, 6), size = 1)は「c(1, 2, 3, 4, 5, 6)から1つの要素を無作為に抽出せよ」という意味である(乱数生成を利用したシミュレーションについては後の章で詳しく説明する)。サイコロの目が出たら、その目をtotalの値に足す (コードの6行目)。
その後、「1回目のサイコロ投げの結果: 5 (これまでの総和: 5)」のように、1回の処理の結果を表示する。 (コードの8行目)。最後にtrialの値を1増やし (コードの14行目)、1度の処理が終了する。
1度の処理が終わったら、whileループの条件判断に戻り、条件が満たされていれば再び処理を繰り返す。これを条件が満たされなくなるまで繰り返す。
この反復処理は、forループで再現することもできる。次のようにすればよい。
total <- 0
for (trial in 1:30) {
die <- sample(1:6, 1)
total <- total + die
# print()の代わりにsprintf()を使うことも可能
result <- sprintf("%d回目のサイコロ投げの結果: %d (これまでの総和: %d)",
trial, die, total)
print(result)
if (total >= 30) break() # ()は省略可能
}[1] "1回目のサイコロ投げの結果: 1 (これまでの総和: 1)"
[1] "2回目のサイコロ投げの結果: 4 (これまでの総和: 5)"
[1] "3回目のサイコロ投げの結果: 3 (これまでの総和: 8)"
[1] "4回目のサイコロ投げの結果: 2 (これまでの総和: 10)"
[1] "5回目のサイコロ投げの結果: 1 (これまでの総和: 11)"
[1] "6回目のサイコロ投げの結果: 4 (これまでの総和: 15)"
[1] "7回目のサイコロ投げの結果: 3 (これまでの総和: 18)"
[1] "8回目のサイコロ投げの結果: 5 (これまでの総和: 23)"
[1] "9回目のサイコロ投げの結果: 3 (これまでの総和: 26)"
[1] "10回目のサイコロ投げの結果: 4 (これまでの総和: 30)"
このコードの中身を説明しよう。事前にサイコロを投げる回数はわからないが、6面サイコロの場合30回以内には必ず合計が30になるので、trial in 1:30としておく。あとはwhileループの書き方とほぼ同じだが、forループではiが自動的に更新されるのでi <- i + 1は不要である。さらに、一定の条件が満たされるときにループを停止する必要がある。そこで登場するのが条件 if とbreak()である。条件分岐は次節で説明するが、ifから始まる行のコードは、「totalが30以上になったらループから脱出せよ」ということを意味する。このようにループからの脱出を指示する関数が break()だ。break() 関数は()を省略して breakと書いてもよい。
これは余談だが、結果の表示に今回はこれまで使ってきたprint()とpaste0()(またはpaste())の代わりに、sprintf()を使っている。sprintf() 内の%dはその位置に指定された変数の値を整数として代入することを意味する。sprintf()にの引数にはまず出力する文字列を指定し、次に代入する変数を順番に入力する。%sは文字 (string) 型、%fは実数 (floating-point number) を意味する。%fでは小数の桁数も指定可能であり、小数第2位まで表示させるには %.2f のように表記する。以下のコードは3つの変数の値と文字列を結合する処理をprint(paste0())とsprintf()を用いて書いたもので、同じ結果が得られる。
name <- "Song"
bowls <- 50
height <- 176.2
print(paste0(name, "がひと月に食べるラーメンは", bowls, "杯で、身長は", height, "cmです。"))[1] "Songがひと月に食べるラーメンは50杯で、身長は176.2cmです。"
# %sにnameを、%dにbowlsを、%.1fにheightを小数第1位まで格納し、出力
sprintf("%sがひと月に食べるラーメンは%d杯で、身長は%.1fcmです。", name, bowls, height)[1] "Songがひと月に食べるラーメンは50杯で、身長は176.2cmです。"
ただし、{}内のsprintf()はそのまま出力されないため、一旦、オブジェクトとして保存し、それをprint()を使って出力する必要がある。詳細については、?sprintfを参照されたい。
今回例として挙げたサイコロ投げは「多くても30回以内に終わる」ことが分かっていたの forループで書き換えることができた。しかし、終了までに必要な反復回数は未知であることもある。目的に応じて forとwhileを使い分けることが求められる。
11.4 条件分岐
11.4.1 if、else if、else による条件分岐
この節では条件分岐について説明する。条件分岐は、条件に応じて異なる処理を行いときに利用する。if による条件分岐の基本形は次のとおりである。
whileを使ったループによく似ているが、while文は条件が満たされる限り{}の内容を繰り返し実行するのに対し、if 文では条件が満たされれときに{}の内容が1回だけ実行されて処理が終了するという違いがある。たとえば、名前が格納されているオブジェクトnameの中身が"Song"なら「ラーメン大好き」と出力されるコードを考えてみよう。
if文の ()内の条件は、「nameの中身が"Song"」の場合のみ{}内の処理を実行せよということを意味する。nameの中身が"Yanai"ならどうなるだろうか。
何も表示されない。このように、if文単体だと、条件が満たされない場合には何も実行されない。
条件にに応じて異なる処理を実行するために、else を使う。これはifとセットで使われるもので、ifの条件が満たされなかった場合の処理内容を指定することができる。elseを加えると、次のようなコードが書ける。
else文は、以下のように改行して書くこともできる。
しかし、この書き方はあまり良くない。else は必ず if とセットで用いられるので、if の処理の終わりである } の直後に半角スペースを1つ挟んで書くのが標準的なコーディングスタイルである。
それでは nameが"Song"でない場合には、「ラーメン好きじゃない」表示されるコードを書いてみよう。
nameが"Song"の場合、前と変わらず「ラーメン大好き」が出力される。nameを"Yanai"に変えてみよう。
[1] "ラーメン好きじゃない"
「ラーメン好きじゃない」が表示される。
しかし、世の中はと「ラーメン大好き」と「ラーメン好きじゃない」のみで構成されているわけではない。「ラーメン大好き」なのはSong と Koike、「ラーメン好きじゃない」のは Yanai のみで、それ以外の人は「ラーメンそこそこ好き」だとしよう。つまり3つのパタンがある。それぞれに別の処理を実行するには、3つ以上の条件に対応できる条件分岐が必要になる。
そのような場合には else ifをifとelseの間に挿入する。
if (条件1) {
条件1が満たされた場合の処理内容
} else if (条件2) {
条件1が満たされず、条件2が満たされた場合の処理内容
} else if (条件3) {
条件1, 2が満たされず、条件3が満たされた場合の処理内容
} else {
条件が全て満たされなかった場合の処理内容
}このように条件分岐を書くと、if文の条件が満たされれば最初の{}内の処理を実行し、満たされなかったら次のelse ifの条件を判定する。その条件が満たされれば{}の処理を実行し、満たされなければ次のelse ifへ移動…を順に実施する。どの条件も満たされないときは、最終的に else の内容が実行される。
それでは実際にコードを書いてみよう。
name <- "Song"
if (name == "Song" | name == "Koike") {
# 上の条件は、(name %in% c("Song", "Koike")) でもOK
print("ラーメン大好き")
} else if (name == "Yanai") {
print("ラーメン好きじゃない")
} else {
print("ラーメンそこそこ好き")
}[1] "ラーメン大好き"
このコードでは、まずnameが"Song" または "Koike" か否かを判定し、TRUEなら「ラーメン大好き」を表示する。FALSE なら次の else if文へ移動する。ここではNameが"Yanai"かどうかを判定する。
最初の条件に登場する |は「OR (または)」を意味する論理演算子であり、第7章で解説した。このように、条件の中で|や&などの論理演算子を使うことで、複数の条件を指定することができる。また、name == "Song" | name == "Koike"はname %in% c("Song", "Koike")に書き換えることがでる。x %in% yはxがyに含まれているか否かを判定する演算子である。たとえば、"A" %in% c("A", "B", "C")の結果はTRUEだが、"Z" %in% c("A", "B", "C")の結果はFALSEになる。
それではnameを"Yanai"、"Koike"、"Shigemura"、"Hakiai"に変えながら結果を確認してみよう。
name <- "Yanai"
if (name %in% c("Song", "Koike")) {
print("ラーメン大好き")
} else if (name == "Yanai") {
print("ラーメン好きじゃない")
} else {
print("ラーメンそこそこ好き")
}[1] "ラーメン好きじゃない"
name <- "Koike"
if (name %in% c("Song", "Koike")) {
print("ラーメン大好き")
} else if (name == "Yanai") {
print("ラーメン好きじゃない")
} else {
print("ラーメンそこそこ好き")
}[1] "ラーメン大好き"
name <- "Shigemura"
if (name %in% c("Song", "Koike")) {
print("ラーメン大好き")
} else if (name == "Yanai") {
print("ラーメン好きじゃない")
} else {
print("ラーメンそこそこ好き")
}[1] "ラーメンそこそこ好き"
name <- "Hakiai"
if (name %in% c("Song", "Koike")) {
print("ラーメン大好き")
} else if (name == "Yanai") {
print("ラーメン好きじゃない")
} else {
print("ラーメンそこそこ好き")
}[1] "ラーメンそこそこ好き"
if文が単独で使われることもそれほど多くない。if文は主に反復処理を行うforループまたはwhile()ループの中、もしくは次節で説明する自作関数内に使われることが多い。上の例では名前からラーメンに対する情熱を判定するために何度も同じコードを書いてきたが、同じコードを繰り返し書くのは効率が悪い。繰り返し行う作業を関数としてまとめれば便利である。関数の作成については第12章で説明する。
ここではforループと条件分岐の組み合わせについて考えてみよう。学生10人の成績が入っているベクトルscoresがあるとしよう。そして、成績が60点以上なら「合格」を、未満なら「不合格」を返すようなコードを書く。この作業を効率的に行うために、 forループとif文を組み合わせる方法を考える。forループではインデクス変数iに1から10までの数字を代入することを繰り返す。ここでの10はベクトルscoresの長さなので、seq_along() を使う。そして、scoresのi番目要素に対して60点以上か否かの判定を行い、「合格」または「不合格」という結果を返す。
以上の内容を実行するコードは、次のように書ける。
scores <- c(58, 100, 81, 97, 71, 61, 47, 60, 73, 85)
for (i in seq_along(scores)) {
if (scores[i] >= 60) {
print(paste0("学生", i, "の判定結果: 合格"))
} else {
print(paste0("学生", i, "の判定結果: 不合格"))
}
}[1] "学生1の判定結果: 不合格"
[1] "学生2の判定結果: 合格"
[1] "学生3の判定結果: 合格"
[1] "学生4の判定結果: 合格"
[1] "学生5の判定結果: 合格"
[1] "学生6の判定結果: 合格"
[1] "学生7の判定結果: 不合格"
[1] "学生8の判定結果: 合格"
[1] "学生9の判定結果: 合格"
[1] "学生10の判定結果: 合格"
このコードは、以下の処理を行っている。
- 1行目: まず、ベクトル
scoresを定義する。 - 3行目:
for文でループを作る。任意の変数としてインデクスiを使う。ベクトルの各要素をのインデクスを入れ替えながら反復を行います。したがって、i in 1:10でも問題ないが、ここではi in seq_along(scores)を利用する。こうすることで、scoresベクトルの長さが変わっても、forの内容を修正する必要がなくなる。 - 4, 5行目:
scoresのi番目要素が60点以上かどうかを判定し、TRUEなら「学生iの判定結果: 合格」を表示する。 - 6-8行目:
scoresのi番目要素が60点以上でない場合、「学生iの判定結果: 不合格」を表示する。
print()内でpaste0()を使うとコードの可読性がやや落ちるので、sprintf()を使ってもよいかもしれない(第13章で説明するパイプ演算子 |> を使えば、可読性の問題は解決する)。
scores <- c(58, 100, 81, 97, 71, 61, 47, 60, 73, 85)
for (i in seq_along(scores)) {
if (scores[i] >= 60) {
result <- sprintf("学生%dの判定結果: 合格", i)
} else {
result <- sprintf("学生%dの判定結果: 不合格", i)
}
print(result)
}[1] "学生1の判定結果: 不合格"
[1] "学生2の判定結果: 合格"
[1] "学生3の判定結果: 合格"
[1] "学生4の判定結果: 合格"
[1] "学生5の判定結果: 合格"
[1] "学生6の判定結果: 合格"
[1] "学生7の判定結果: 不合格"
[1] "学生8の判定結果: 合格"
[1] "学生9の判定結果: 合格"
[1] "学生10の判定結果: 合格"
次に、結果を出力するのではなく、結果を別のベクトルに格納する例を考えてみよう。あらかじめ scores と同じ長さの空ベクトルpfを用意します。ここに各学生について合否判定の結果を「合格」または「不合格」として格納する処理を行う。以下のようなコードが書ける。
このコードでは、scores を定義した後、中身がNAのみで構成される長さがscores と同じベクトルpfを定義する。rep(NA, length(scores))はNAをlength(Scores)個(ここでは10個)並べたベクトルを生成する。 続いて、点数によって条件分岐を行い、メッセージを出力するのではなく、pfのi番目に"合格"か"不合格"を入れる。結果を確認してみよう。
このように、if文は反復処理を行うforループまたはwhileループと組み合わせることでその本領を発揮する。実は、今回の例については次に説明するifelse()を使えば1行で処理することができる。しかし、複雑な処理を伴う反復と条件分岐を組み合わせるには、今回のようにforループとif文を組み合わせるのが有効である。
11.4.2 ifelse()による条件分岐
ifelse()は与えられたベクトル内の各要素に対して条件分岐を行い、それをベクトルの全要素について繰り返す関数である。簡単な条件分岐と反復処理を同時に行う非常に便利な関数である。ifelse()の基本的な書き方は以下のとおり。
たとえば、学生10人の成績が入っているベクトルscoresに対し、60点以上の場合に合格 ("Pass")、60点未満の場合に不合格 ("Fail") の値を割り当て、pfというベクトルに格納しよう。
[1] "Fail" "Pass" "Pass" "Pass" "Pass" "Pass" "Fail" "Pass" "Pass" "Pass"
条件はscoresの値が60以上か否かであり、60以上なら"Pass"を、それ以外なら"Fail"を返す。
返す値として新しい値を与える代わりに、一定の条件が満たされたら現状の値を保持し、それ以外なら指定した値に変更して返すこともできる。たとえば、世論調査では以下のように「答えたくない」または「わからない」を9や99などで記録することが多い。この値はこのままでは分析するのが難しいので、欠損値として扱いたいことがある。例として、次の質問を考えよう。
Q. あなたはラーメンが好きですか。
- 1.大好き
- 2.そこそこ好き
- 3.好きではない
- 9.答えたくない
この質問に対する10人の回答が格納されたデータフレーム (Ramen) があるとしよう。このデータフレームには2つの列があり、id 列は回答者のID(識別番号)、ramen 列にはは上の質問に対する答えが入ってる。
このramen列に対し、ramen == 9ならNAを割り当て、それ以外の場合は元の値をそのまま残したいとしよう。そしてその結果をRamenのramen列に上書する。これは次のコードで実行できる。
id ramen
1 1 1
2 2 1
3 3 2
4 4 1
5 5 3
6 6 1
7 7 NA
8 8 2
9 9 1
10 10 NA
3つ以上のパタンに分岐させたいときは、ifelse()の中でifelse()を使うこともできる。scoresベクトルの例を考えてよう。ここで90点以上ならS、80点以上ならA、70点以上ならB、60点以上ならCを割り当て、それ以外はFを返すことにしよう。そして、その結果をgrade という変数に格納してみよう。
このコードでは、ifelse()の条件がFALSEの場合、次のifelse()での判定を行う。結果を確認しておこう。
この例からもわかるとおり、ifelse()を使いすぎるとコードの可読性が低くなるので、このようなコードはあまり推奨できない。複数の条件に応じて返す値を変える処理は、{dplyr}パッケージのcase_when()関数で行うのがよいだろう。この関数については、第14章で詳細に説明するが、ここでは上のコードと同じ処理を実施する例のみ示す。
11.4.3 switch()による条件分岐
switch()は、与えられた長さ1の文字列11を用いた条件分岐を行う。これが単体で使われうことはほとんどなく、通常は関数のなかで使われる。したがって、初めて本書を読む場合はこの節を一旦とばし、第12章を読んだ後に必要に応じて戻ってきてほしい。
ここでは2つの数値xとyの加減乗除を行う関数m_calc という名前の関数を作る。この関数はx とy以外にoperationという引数をもち、このoperationの値によって行う処理が変わる。たとえば、operation = "+"なら足し算を、operation = "*"なら掛け算を行う。
このコードの中身を確認しよう。重要なのは2行目から8行目の部分である。switch()の最初の引数は条件を判定する長さ1のベクトルである。ここではこれがoperationである。そして、operationに指定される実引数の値によって異なる処理を行う。"+" = x + yは「operationの値が"+"なら、x + yを実行する」ことを意味する。これを他の演算子に対しても指定する。最後の引数は、どの条件にも合わない場合の処理である。ここでは, operation の値に使えるのは +, -, *, / のみです。"というエラーメッセージを表示し、関数の実行を停止するために stop() 関数を指定している。
上のコードは、if、else if、 elseを使って書き換えることができる。
先ほどと異なる点は、function()の中でoperationのデフォルト値をベクトルとしてした与えたことだ。これは「operation引数の値がこれらの値以外になることは許さない」ということを意味する。match.arg()関数は、operation引数が予め指定された引数の値と一致するか否かを判断する。指定された引数と一致しない場合、エラーを表示し、処理を中断する。ちなみに、match.arg()はswith()文と組み合わせて使うこともできる。
今回の例の場合、switch()を使ったほうがコードの内容がわかりやすく、読みやすいが、ifによる条件分岐でも十分に対応可能である。また、条件によって行う処理が複雑な場合にはswitch()よりもifのほうが使いやすい。
作った関数を使ってみよう。
[1] 8
[1] 2
[1] 15
[1] 1.666667
では"+"、"-"、"*"、"/"以外の値をoperationに与えるとうなるだろうか。ためしに"^"を入れてみよう。
Error in my_calc(5, 3, operation = "^"): operation の値に使えるのは +, -, *, / のみです。
stop()内に書いたエラーメッセージが表示され、処理が中止される。
11.5 練習問題
問1
- 3つの6面サイコロ投げを考える。3つの目の和が15になるまでサイコロ投げを繰り返す。どのような目が出て、その合計はいくつか。そして、何回目のサイコロ投げかを表示するコードを
forループを用いて書きなさいwhileループを用いて書きなさい- 上の2つのコードが同じ結果を表示することを確認したうえで、どちらの方法がより効率的か考察しなさい。
[1] "1目のサイコロ投げの結果: 4, 2, 6 (合計: 12)"
[1] "2目のサイコロ投げの結果: 5, 4, 1 (合計: 10)"
[1] "3目のサイコロ投げの結果: 5, 6, 4 (合計: 15)"
問2
- 以下のような長さ5の文字型ベクトル
causeがある。「肥満の原因はXXでしょう。」というメッセージを表示するコードを書きなさい。ただし、「XX」の箇所にはベクトルの各要素を代入すること。表示されるメッセージは5個でなければならない。
[1] "肥満の原因は喫煙でしょう。"
[1] "肥満の原因は飲酒でしょう。"
[1] "肥満の原因は食べすぎでしょう。"
[1] "肥満の原因は寝不足でしょう。"
[1] "肥満の原因はストレスでしょう。"
- 長さ4の文字型ベクトル
effectを以下のように定義する。このとき、「YYの原因はXXでしょう。」というメッセージを表示するコードを書きなさい。ただし、「YY」にはeffectの要素を、「XX」にはcauseの要素を代入すること。出力されるメッセージは20個でなければならない。
- 長さ3の文字型ベクトル
solutionを以下のように定義する。このとき、「YYの原因はXXですが、ZZ改善されるでしょう。」というメッセージを出力するコードを書きなさい。ただし、「YY」にはeffectの要素を、「XX」にはcauseの要素を、「ZZ」にはsolutionの要素を代入すること。出力されるメッセージは60個でなければならない。
問3
- 長さ2のnumericベクトル
dataについて考える。条件分岐を用いてdataの1番目の要素 (data[1]) が2番目の要素 (data[2]) より大きい場合、1番目の要素と2番目の順番を逆転させる条件分岐を作成せよ。たとえば、data <- c(5, 3)なら、条件分岐後のdataの値がc(3, 5)になるようにするコードを書きなさい。
具体的にはメモリの特定箇所にデータを書き込んだり、それを読み取んだりすること。↩︎
チューリング完全 (Turing-complete) な言語の条件に「反復」はない。条件分岐でループを実行することができるからだ。よって、プログラミングを構成するのは「データの読み書き」と「条件分岐」のみとも考えられる。↩︎
並べ替え(ソート)アルゴリズムはプログラミングを学習する際に定番の題材であり、様々なアルゴリズムがある。↩︎
苦行であるだけでなく、スプレッドシート上でデータを書き換えると、入力ミスをおかしやすいし、そのミスを後で見つけるのが非常に困難である。したがって、データをスプレッドシート上で編集するのは絶対にやめるべきである。↩︎
RはOOPを実装するためにS3クラスをデフォルトで採用しているが、これはオブジェクト指向プログラミング言語としては厳密性を欠くところがある。Rでは、S3の代わりにS4やR6などの現代的なOOPを実装することもできる。↩︎
厳密には、tibble型データは3つのクラスを内部に有しており、その中にdata.frameが含まれる。↩︎
チェーンケースは、COBOLやLISPのような言語では使われる。どうしても使いたければ、変数名全体をバックティック(
`)で囲んで`math-score` <- 10のようにする方法もあるが、もちろん推奨しない。↩︎paste()関数を使うと、sepで指定した文字列で繋ぐ。sepの既定値は半角スペース。↩︎しかし、forループをあまりにも多く重ねるとコードの可読性が低下するので注意しよう。多重の反復処理が必要な場合には、反復処理のための関数を自作することで対応することもできる。↩︎
交換法則が成り立ち、演算子の左右を区別する意味はないので、本当は81パタンも考える必要はない。↩︎
長さ1の数値型ベクトルにも使えるが、使われる例があまりない。↩︎