5  Rでプログラミング

ここではプログラミング言語としてのRについて解説する。 なお副読本として 小杉, 紀ノ定, and 清水 (2023) を挙げておく。また,プログラミングのより専門的な理解のために,ランダー,J.P. ([2017] 2018), 株式会社ホクソエム ([2016] 2017), 石田 et al. ([2015] 2016) なども参考にすると良い。

プログラミング言語は,古くはCやJava,最近ではPythonやJuliaなどがよく用いられている。Rも統計パッケージというよりプログラミング言語として考えるのが適切かもしれない。Rは他のプログラミング言語に比べて,変数の型宣言を事前にしなくても良いことや,インデントなど書式についておおらかなところは,初心者にとって使いやすいところだろう。一方で,ベクトルの再利用のところで注意したように(Section 2.5.1),不足分を補うために先回りして補填されたり,この後解説する関数の作成時に明示的な指定がなければ環境変数を参照する点など,親切心が空回りするところがある。より厳格な他言語になれていると,こうした点はかえって不便に思えるところもあるかもしれない。総じて,R言語は初心者向けであるといえるだろう。

さて,世にプログラミング言語は多くあれど1,その全てに精通する必要はないし,不可能である。それよりも,プログラミング言語一般に通底する基本的概念を知り,あとは各言語による「方言」がある,と考えた方が生産的である。その基本的概念を3つ挙げるとすれば,「代入」「反復」「条件分岐」になるだろう。

5.1 代入

代入は,言い換えればオブジェクト(メモリ)に保管することを指す。これについては既に Chapter 2 で触れた通りであり,ここでは言及しない。オブジェクトや変数の型,常に上書きされる性質に注意しておけば十分だろう。

一点だけ追加で説明しておくと,次のような表現がなされることがある。

a <- 0
a <- a + 1
print(a)
[1] 1

ここではあえて,代入記号として=を使った。2行目にa = a + 1 とあるが,これを見て数式のように解釈しようとすると混乱する。数学的には明らかにおかしな表現だが,これは上書きと代入というプログラミング言語の特徴を使ったもので,「(いま保持している)aの値に1を加えたものを,(新しく同じ名前のオブジェクト)aに代入する(=上書きする)」という意味である。この方法で,a をカウンタ変数として用いることがある。誤読の可能性を下げるため,この授業においては代入記号を<-としている。

このオブジェクトを上書きするという特徴は多くの言語に共通したものであり,間違いを避けるためには,オブジェクトを作る時に初期値を設定することが望ましい。先の例では,代入の直上でa <- 0としており,オブジェクトa0 を初期値として与えている。この変数の初期化作業がないと,以前に使っていた値を引き継いでしまう可能性があるので,今から新しく使う変数を作りたいというときは,このように明示しておくといいだろう。

なお,変数をメモリから明示的に削除する場合は,remove関数を使う。

remove(a)

これを実行すると,RStudioのEnvironmentタブからオブジェクトaが消えたことがわかるだろう。 メモリの一斉除去は,同じくRStudioのEnvironmentタブにある箒マークをクリックするか,remove(list=ls())とすると良い2

5.2 反復

5.2.1 for文

電子計算機の特徴は,電源等のハードウェア的問題がなければ疲労することなく計算を続けられるところにある。人間は反復によって疲労が溜まったり,集中力が欠如するなどして単純ミスを生成するが,電子計算機にそういったところはない。

このように反復計算は電子計算機の中心的特徴であり,細々した計算作業を指示した期間反復させ続けることができる。反復の代表的なコマンドはforであり,forループなどと呼ばれる。forループはプログラミングの基本的な制御構造であり,R言語のforループの基本的な構文は次のようになる:

for (value in sequence) {
    # 実行するコード
}

ここのvalueは各反復でsequenceの次の要素を取る反復インデックス変数である。。sequenceは一般にベクトルやリストなどの配列型のデータであり,「#実行するコード」はループ体内で実行される一連の命令になる。

以下はfor文の例である。

for (i in 1:5) {
  cat("現在の値は", i, "です。\n")
}
現在の値は 1 です。
現在の値は 2 です。
現在の値は 3 です。
現在の値は 4 です。
現在の値は 5 です。

for 文は続く小括弧のなかである変数を宣言し(ここではi),それがどのように変化するか(ここでは1:5,すなわち1,2,3,4,5)を指定する。続く中括弧の中で,反復したい操作を記入する。今回はcat 文によるコンソールへの文字力の出力を行っている。ここでのコマンドは複数あってもよく,中括弧が閉じられるまで各行のコマンドが実行される。

次に示すは,sequenceにあるベクトルが指定されているので,反復インデックス変数が連続的に変化しない例である。

for (i in c(2, 4, 12, 3, -6)) {
  cat("現在の値は", i, "です。\n")
}
現在の値は 2 です。
現在の値は 4 です。
現在の値は 12 です。
現在の値は 3 です。
現在の値は -6 です。

また,反復はネスト(入れ子)になることもできる。次の例を見てみよう。

# 2次元の行列を定義
A <- matrix(1:9, nrow = 3)

# 行ごとにループ
for (i in 1:nrow(A)) {
  # 列ごとにループ
  for (j in 1:ncol(A)) {
    cat("要素 [", i, ", ", j, "]は ", A[i, j], "\n")
  }
}
要素 [ 1 ,  1 ]は  1 
要素 [ 1 ,  2 ]は  4 
要素 [ 1 ,  3 ]は  7 
要素 [ 2 ,  1 ]は  2 
要素 [ 2 ,  2 ]は  5 
要素 [ 2 ,  3 ]は  8 
要素 [ 3 ,  1 ]は  3 
要素 [ 3 ,  2 ]は  6 
要素 [ 3 ,  3 ]は  9 

ここで,反復インデックス変数がijというように異なる名称になっていることに注意しよう。例えば今回,ここで両者をiにしてしまうと,行変数なのか列変数なのかわからなくなってしまう。また少し専門的になるが,R言語はfor文で宣言されるたびに,内部で反復インデックス変数を新しく生成している(異なるメモリを割り当てる)ためにエラーにならないが,他言語の場合は同じ名前のオブジェクトと判断されることが一般的であり,その際は値が終了値に到達せず計算が終わらないといったバグを引き起こす。反復に使う汎用的な変数名としてi,j,kがよく用いられるため,自身のスクリプトの中でオブジェクト名として単純な一文字にすることは避けた方がいいだろう。

5.2.2 while文

whileループはプログラミングの基本構造であり,特定の条件が真(True)である間,繰り返し一連の命令を実行する。「“while”(~する間)」という名前から直感的に理解できるだろう。

R言語のwhileループの基本的な構文は次のようになる:

while (condition) {
    # 実行するコード
}

ここで,「condition」はループが終了するための条件である。「# 実行するコード」はループ体内で実行される一連の指示である。たとえば,1から10までの値を出力するwhileループは以下のように書くことができる:

i <- 1
while (i <= 5) {
  print(i)
  i <- i + 1
}
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5

このコードでは,「i」が5以下である限りループが続く。「print(i)」で「i」の値が表示され,「i <- i + 1」で「i」の値が1ずつ増加する。これにより,「i」の値が10を超えると条件が偽(False)となり,ループが終了する。

whileループを使用する際の一般的な注意点は,無限ループ(終わらないループ)を避けることである。これは,conditionが常に真(True)である場合に発生する。そのような状況を避けるためには,ループ内部で何らかの形でconditionが最終的に偽(False)となるようにコードを記述することが必要である。

また,R言語は他の多くのプログラミング言語と異なり,ベクトル化された計算を効率的に行う設計がされている。したがって,可能な限りforループやwhileループを使わずに,ベクトル化した表現を利用すれば計算速度を上げることができる。

5.3 条件分岐

条件分岐はプログラム内で特定の条件を指定し,その条件が満たされるかどうかによって異なる処理を行うための制御構造である。R言語では if-else を用いて条件分岐を表現する。

5.3.1 if 文の基本的な構文

以下が if 文の基本的な構文になる:

if (条件) {
    # 条件が真である場合に実行するコード
}

if の後の小括弧内に条件を指定する。この条件が真(TRU)であれば,その後の 中括弧{} 内のコードが実行される。さらに,else を使用して,条件が偽(FALSE)の場合の処理を追加することもできる:

if (条件) {
    # 条件が真である場合に実行するコード
} else {
    # 条件が偽である場合に実行するコード
}

以下に具体的な使用例を示そう:

x <- 10

if (x > 0) {
  print("x is positive")
} else {
  print("x is not positive")
}
[1] "x is positive"

このコードでは,変数 x が正の場合とそうでない場合で異なるメッセージを出力する。

条件は論理式(例:x > 0, y == 1)や論理値(TRUE/FALSE)を返す関数・操作(例:is.numeric(x))などで指定する。また,複数の条件を組み合わせる際には論理演算子(&&, ||)を使用する。

この例では,xが正とyが負の場合に特定のメッセージを出力する。それ以外の場合は,「Other case」と出力される。xyの値を色々変えて,試してみて欲しい。

x <- 10
y <- -3

if (x > 0 && y < 0) {
  print("x is positive and y is negative")
} else {
  print("Other case")
}
[1] "x is positive and y is negative"

5.4 反復と条件分岐に関する練習問題

  1. 1から20までの数字で,偶数だけをプリントするプログラムを書いてください。
  2. 1から40までの数値をプリントするプログラムを書いてください。ただしその数値に3がつく(1か10の位の値が3である)か,3の倍数の時だけ,数字の後ろに「サァン!」という文字列をつけて出力してください。
  3. ベクトル c(1, -2, 3, -4, 5) の各要素について,正なら “positive”,負なら “negative” をプリントするプログラムを書いてください。
  4. 次の行列\(A\)\(B\)の掛け算を計算するプログラムを書いてください。なお,Rで行列の積は%*%という演算子を使いますが,ここではfor文を使ったプログラムにしてください。出来上がる行列の\(i\)\(j\)列目の要素\(c_{ij}\)は,行列\(A\)の第\(i\)行の各要素と,行列\(B\)の第\(j\)列目の各要素の積和,すなわち\[c_{ij}=\sum_{k} a_{ik}b_{kj}\]になります。検算用のコードを下に示します。
A <- matrix(1:6, nrow = 3)
B <- matrix(3:10, nrow = 2)
## 課題になる行列
print(A)
     [,1] [,2]
[1,]    1    4
[2,]    2    5
[3,]    3    6
print(B)
     [,1] [,2] [,3] [,4]
[1,]    3    5    7    9
[2,]    4    6    8   10
## 求めるべき答え
C <- A %*% B
print(C)
     [,1] [,2] [,3] [,4]
[1,]   19   29   39   49
[2,]   26   40   54   68
[3,]   33   51   69   87

5.5 関数を作る

複雑なプログラムも,ここまでの代入、反復,条件分岐の組み合わせからなる。回帰分析や因子分析のような統計モデルを実行するときに,統計パッケージのユーザとしては,統計モデルを実現してくれる関数にデータを与えて答えを受け取るだけであるが,そのアルゴリズムはこれらプログラミングのピースを紡いでつくられているのである。

ここでは関数を自分で作ることを考える。といっても身構える必要はない。表計算ソフトウェアで同じような操作を繰り返すときにマクロに記録するように,R上で同じようなコードを何度も書く機会があるならば,それを関数という名のパッケージにしておこう,ということである。関数化しておくことで手続きをまとめることができ,小単位に分割できるため並列して開発したり,バグを見つけやすくなるという利点がある。

5.5.1 基本的な関数の作り方

関数が受け取る値のことを引数(ひきすう,argument)といい,また関数が返す値のことを戻り値(もどりち,value)という。\(y=f(x)\)という式は,引数がxで戻り値がyな関数\(f\),と言い換えることができるだろう。

Rの関数を書く基本的な構文は以下のようになる。

function_name <- function(argument) {
   # function body
   return(value)
}

ここでfunction bodyとあるのは計算本体である。例えば与えられた数字に3を足して返す関数,add3を作ってみよう。プログラムは以下のようになる。

add3 <- function(x) {
  x <- x + 3
  return(x)
}
# 実行例
add3(5)
[1] 8

また,2つの値を足し合わせる関数は次のようになる。

add_numbers <- function(a, b) {
  sum <- a + b
  return(sum)
}
# 実行例
add_numbers(2, 5)
[1] 7

ここで示したように,引数は複数取ることも可能である。また,既定値default valueを設定することも可能である。次の例を見てみよう。

add_numbers2 <- function(a, b = 1) {
  sum <- a + b
  return(sum)
}
# 実行例
add_numbers2(2, 5)
[1] 7
add_numbers2(4)
[1] 5

関数を作るときに,(a,b=1)としているのは,bに既定値として1を与えていて,特に指定がなければこの値を使うよう指示しているということである。実行例において,引数が2つ与えられている場合はそれらを使った計算をし(2+5),1つしか与えられていない場合は第一引数aに与えられた値を,第二引数bは既定値を使った計算をする(4+1),という挙動になる。

ここから推察できるように,われわれユーザが使う統計パッケージの関数にも実は多くの引数があり,既定値が与えられているということだ。これらは選択的に,あるいは能動的に与えることができるものであるが,これらの引数は選択的に指定することができるのだが,通常は一般的に使われる値や計算の細かな設定に関するものであり,開発者がユーザの手間を省くために提供しているものである。関数のヘルプを見ると指定可能な引数の一覧が表示されるので,ぜひ興味を持って見てもらいたい。

5.5.2 複数の戻り値

Rでの戻り値は1つのオブジェクトでなければならない。しかし,複数の値を返したいということがあるだろう。そのような場合は,返すオブジェクトをlistなどでひとまとめにして作成すると良い。以下に簡単な例を示す。

calculate_values <- function(a, b) {
  sum <- a + b
  diff <- a - b
  # 戻り値として名前付きリストを作成
  result <- list("sum" = sum, "diff" = diff)
  return(result)
}
# 実行例
result <- calculate_values(10, 5)
# 結果を表示
print(result)
$sum
[1] 15

$diff
[1] 5

5.6 課題

  1. ある値を与えたとき,正の値なら”positive”,負の値なら”negative”,0のときは”Zero”と表示する関数を書いてください。
  2. ある2組の数字を与えた時,和,差,積,商を返す関数を書いてください。
  3. あるベクトルを与えた時,算術平均,中央値,最大値,最小値,範囲を返す関数を書いてください。
  4. あるベクトルを与えた時,標本分散を返す関数を書いてください。なおRの分散を返す関数varは不偏分散\(\hat{\sigma}\)を返しており,標本分散vとは計算式が異なります。念のため,計算式を以下に示します。 \[\hat{\sigma} = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})^2 \] \[v= \frac{1}{n}\sum_{i=1}^n (x_i - \bar{x})^2 \]

  1. シ (2016) には117種もの計算機言語が紹介されている。↩︎

  2. ls()関数はlist objectsの意味で,メモリにあるオブジェクトのリストを作る関数↩︎