「大人の教養・知識・気付き」を伸ばすブログ

一流の大人(ビジネスマン、政治家、リーダー…)として知っておきたい、教養・社会動向を意外なところから取り上げ学ぶことで“気付く力”を伸ばすブログです。目下、データ分析・語学に力点を置いています。今月(2022年10月)からは多忙につき、日々の投稿数を減らします。

MENU

プログラムとしてのRを学ぶ(その02/16)

 \mathrm{R}をプログラムとして見たときに注意・検討すべきところを学んでおきたい、ということで

を読んでいく。

2. ベクトル

 \mathrm{R}において基本になるデータ型はベクトルである。\mathrm{R}のベクトルにおいて重要な概念は以下の3つにある:

リサイクル 特定の状況下においてベクトルの長さを自動的に伸長させる。
フィルタリング ベクトルの部分集合を抽出する。
ベクトル化 ベクトルに対して成分(要素)ごとに関数を適用させる。

2.1 スカラー、ベクトル、配列、行列

 \mathrm{R}が他の言語に比較して特徴的な事の1つは、スカラーに相当するデータ型(\mathrm{R}ではモードと呼ぶ。)を持たないことである。すなわち\mathrm{R}スカラーを成分数が1であるようなベクトルとして扱う。

 ベクトルのすべての成分は同じモードでなければならない。もしあるベクトルのモードを知りたければ\mathrm{typeof()}関数を用いればよい。

 \mathrm{R}のベクトルインデックスは1から始める。

2.1.1. ベクトル要素の追加と削除

 \mathrm{R}のベクトルは要素の挿入や削除ができない。ベクトルの大きさは作成時に決まり要素の追加や削除にはベクトルを再度割り当てる。

# 例:ベクトルxについて3番目の要素と4番目の要素の間にスカラーを入れたい
x <- c(88,5,12,13) # 4つの要素を持つベクトルとして定義
x <- c(x[1:3],168,x[4])
x

上記の例の2行目のように、まずベクトルxの1番目から3番目および168xの4番目の成分を持つような要素数5のベクトルを新たに定義し、それをxに割り当てるのである。このようにxはポインタであり、xが新たに作成したベクトルを指すようにすることで再割り当てを実現しているのである。このため、場合によっては\mathrm{R}のパフォーマンス向上を制約しかねない。

2.1.2 ベクトルの長さの取得

 \mathrm{R}においてベクトルの長さを取得するには\mathrm{length}()関数を用いる。
 たとえばあるベクトルxについて特定の値が格納されている最初のインデックスが欲しい場合には\mathrm{for}文にxの要素を直接ループさせては失敗する。そうした場合には1:\mathrm{length(x)}でループを回す方が確実である。
 またエラー処理などで\mathrm{length}(x)0になる可能性を考慮するにも有用である。

2.1.3 ベクトルとしての配列と行列

 \mathrm{R}において配列と行列もクラス属性が付与されたベクトルである。そのため配列と行列にもベクトルに関する説明が当てはまる。

2.2 宣言の必要性

 \mathrm{R}では変数を明示的に予め宣言する必要はない。他方でベクトルの特定要素を参照するには事前にそのベクトルを宣言しなければならない。これはベクトルの個々の要素を読み書きするのに内部で動作している関数が、その対象変数がベクトルであることを事前に知らないとうまく対応できないからである。たとえば

z <- 3 #これは正しい
y[1] <- 3 # これはyを宣言していなければエラー

#####
y <- 1:13
y[1] <- 2 # これはOK

 割り当てをするのにモードによる制約はない。

x <- c(1,5,13)
x <- "abc" # これは許容される

2.3 リサイクル

 2つのベクトルに長さが同じでないといけない演算を適用すると、\mathrm{R}リサイクルを行う。リサイクルとは、長い方の長さに一致するまで短い方のベクトルがその要素を繰り返すことである。

c(1,2,3) + c(1,2,3,4) # この演算はc(1,2,3,1) +c(1,2,3,4) = c(2,4,6,5)になるはずである

x <- matrix(1:6, nrow = 3, ncol =2))
x + c(1,2) #行方向から列方向にベクトルが繰り返される

2.4 一般的なベクトル操作

2.4.1 ベクトルの算術演算と論理演算

 \mathrm{R}関数型言語であるから、すべての演算子は関数である。算術演算子は関数で、これらはベクトルに対しては要素ごとに行われる。

"+"(2,3)  # 2 + 3のこと
5 + c(1,2) # スカラーは要素数が1のベクトルであったから、リサイクルが起きて、前者はc(5,5)として後者の各成分に5が足された値が返ってくる

# 算術演算子は要素ごとに行われる
x <- c(1,2,4)

x * c(5,0,-1) #要素ごとに積を取る
x / c(5,4,-1) # c(0.2, 0.5, -4.0)
x %% c(5,4,-1) # c(1,2,0)
2.4.2 ベクトルのインデックス付け

 \mathrm{R}で最も扱う演算子にベクトルのインデックス付けがある。あるベクトルの特定のインデックス要素を選ぶこと、すなわち\mathrm{vector1}[\mathrm{vector2}]といった形式でサブベクトルを作成できる。\mathrm{length}()関数を組み合わせたりすることで利便性が向上する。

y <- 1:10

y[1:5] # 1,2,3,4,5
v <- 3:4
y[3:4] # 3,4

y[c(3,3,3)] # 3,3,3と重複は許容される
y[c(5,2,1)] # 5,2,1と順番を変えることも可能
y[-1] # 2,3,4,5,6,7,8,9,10 と負値は指定した要素を除くことを意味する
y[1]==y[-c(2:9)] #TRUE
y[-length(y)] # 最終要素以外をすべて取得する
2.4.3 演算子を用いたベクトルの生成

 ベクトルを生成するのに便利な関数がいくつか存在する。

5:8 # 5から8までのすべての整数を要素にそのまま昇順で持つベクトルを返す
5:1 # 5から1までのすべての整数を要素にそのまま降順で持つベクトルを返す

i <- 5
# 以下は異なる値を返すので注意
1:i-1 # c(1,2,3,4,5)の各要素から1が引かれたベクトルになる
1:(i-1) # c(1,2,3,4)

なお\mathrm{R}における演算子の優先順位は?Syntaxで閲覧できる。

2.4.4 seq()

 \mathrm{seq}()は:を汎用化したもので、等差数列を作成する。等差は小数でも問題ない。また\mathrm{for}文のイテレーターを明示的に書きたい場合に、もし\mathrm{length}(x)0だったときの挙動がより望ましい。

x <- NULL

for(i in 1:length(x)){
  print(1) # 2回表示される=ループが回っている
}

for(i in seq(x)){
  print(1) # 一度も表示されない=ループが回っていない
}
2.4.5 rep()

 \mathrm{rep}()を用いると、指定した成分数のすべての成分の値が等しいベクトルを作成できる。

rep(3,3) # c(3,3,3)
rep(c(1,2,3),3) # c(1,2,3,1,2,3,1,2,3)

2.5 all()とany()

 \mathrm{all}(),\mathrm{any}()は引数のすべてもしくは少なくとも1つが真(\mathrm{TRUE})であるか否かを返す。

x <- rep(1,5)

all(x>0) # TRUE
x[1] <- -1
all(x>0) # FALSE

y <- c(rep(-1,5),1)
all(y <-1) # FALSE
any(y < -1) # TRUE
2.5.1 例:連続値を見つける

 0,1が並ぶ文字列における1の連続を見つけるときに、なるべく効率的に行いたい。
 素朴には以下のような関数が考えられる。

### 0,1のみからなる文字列の中で連続した1についてその連続数を塊分だけ取って返す
findruns <- function(x, k){
  n <- length(x)
  runs <- NULL
  
  for(i in 1:(n-k+1)){
    if(all(x[i:(i+k-1)]==1)){
      runs <- c(runs,i)
    }
  }
  return(runs)
}


これは\mathrm{runs}を積み上げる点が望ましくない。ベクトルの割り当ては時間が掛かる処理で、\mathrm{c(runs,i)}を割り当てる度にスクリプトの実行速度が下がる。そこで\mathrm{runs}を長さnのベクトルとして予め宣言すればよい。

### 改良版
findruns1 <- function(x, k){
  n <- length(x)
  runs <- vector(length = n)
  count <- 0
  
  for(i in 1:(n-k+1)){
    if(all(x[i:(i+k-1)]==1)){
      count <- count + 1
      runs[count] <- i
    }
  }
  
  if(count > 0){
    runs <- runs[1:count]
  }else{
    runs <- NULL
  }
  return(runs)
}
2.5.2 例:離散値時系列の予測問題

 雨が降る(=1)、降らない(=0)として記録した時系列データを基に将来の降雨を予測することを考える。ここでは直近k日間の降雨数(1の数)がk/2以上の場合に雨が降ると予測することとする。問題は適当なkを選択することにある。
 ここでは訓練データとして500日間間の天気データを基にたとえばk=3としたときの精度を考えて、最適なkを考える。そこで具体的な関数としてたとえば以下が考えられる:

pred_a <- function(x,k){
  n <- length(x)
  k2 <- k/2
  
  pred <- vector(length = n-k)
  
  for(i in 1:(n-k)){
    if(sum(x[i:(i+(k-1))]) >= k2){
      pred[i] <- 1
    }else{
      pred[i] <- 1
    }
  }
  return(mean(abs(pred - x[(k+1):n])))
}


 上記のスクリプトは簡潔である一方、遅くなる。それはループのベクトル化で解決し得る一方で、\mathrm{sum()}関数が何度も呼ばれることが障害になる。そこでループのそれぞれの反復において以前の合計を更新する方式を取ることで解消できる。

pred_b <- function(x,k){
  n <- length(x)
  k2 <- k/2
  pred <- vector(length = n-k)
  
  sm <- sum(x[1:k])
  
  if(sm >= k2){
    pred[1] <- 1
  }else{
    pred[1] <- 0
  }
  
  if(n-k >= 2){
    for(i in 2:(n-k)){
      sm <- sm + x[i+k-1] -x[i-1]
      if(sm >= k2){
        pred[i] <- 1
      }else{
        pred[i] <- 0
      }
    }
  }
  return(mean(abs(pred - x[(k+1):n])))
}

pred_c <- function(x,k){
  n <- length(x)
  k2 <- k/2
  
  pred <- vector(length = n-k)
  csx <- c(0,cumsum(x))
  
  for(i in 1:(n-k)){
    if(csx[i+k] - csx[i] >= k2){
      pred[i] <- 1
    }else{
      pred[i] <- 0
    }
  }
  
  return(mean(abs(pred - x[(k+1):n])))
}

2.6 ベクトル化された演算

 ベクトルxのすべての要素に適用したい関数f()がある場合、xそのものに対してf()を呼び出しさえすればよい。これはコードを簡潔にするパフォーマンスを大幅に向上させる、という点で非常に望ましい。

2.6.1 ベクトルの入出力
########################
### ベクトルへの演算 ###
########################

## 
u <- c(5,2,8)
v <- c(1,3,9)

u > v

## 
w <- function(x) return(x + 1)
w(u)

## 
y <- c(12,5,13)

#以下は等価: 4はリサイクルされている
y + 4
"+"(y,4)
2.6.2 ベクトル入力と行列出力
####################################
### 行列を出力するベクトル値関数 ###
####################################

z12 <- function(z) return(c(z,z^2))
sapply(1:8, z12)

2.7 NA値とNULL値

 \mathrm{Python}における\mathrm{None}に相当するものが\mathrm{R}には2つある。一方が欠損値(レコードは存在するもののその値が不明なもの)を表す\mathrm{NA}で、もう一方がそもそも値が存在しないことを表す\mathrm{NULL}である。

2.7.1 NAの使用

 \mathrm{R}の多くの統計関数では、欠損値(\mathrm{NA})を飛ばすように指示できる。\mathrm{NA}はデフォルトでは飛ばさない一方で、\mathrm{NULL}は自動的に飛ばす。

2.7.2 NULLの使用

 \mathrm{NULL}は無いものとして扱われる。\mathrm{NA}は欠損を表し、無いわけではない。

##################
### NULLの使用 ###
##################

# NULLの1つの使い方
z <- NULL

for(i in 1:10){
  if(i %% 2 ==0){
    z <- c(z,i)
  }
}

z

# しかし以下の方が自然
seq(2,10,2)
2 * 1:5

# NAではこれはできない
w <- NA

for(i in 1:10){
  if(i %% 2 ==0){
    w <- c(w,i)
  }
}

w

2.8 フィルタリング

 \mathrm{R}におけるベクトルのもう1つの機能としてフィルタリングがある。すなわち「ある条件を満たすベクトル尿素を抽出する」ことができる。

2.8.1 フィルタリング・インデックスの作成

 ブール値を持つベクトルをインデックスとして渡すことでフィルタリングすることができる。

##################################
### フィルタリングインデックス ###
##################################

z <- c(5,2,-3,8)

# インデックスを作ってみる
flg <- z^2>8

z[flg]

# flgは以下と等価で「ベクトル」に関数を適用している
">"(z^2,8)

# 割り当てもインデックスを用いて簡単にできる
z[z<3] <- 0
2.8.2 subset()関数を使ったフィルタリング

 \mathrm{subset}()関数を用いてもフィルタリングできる。\mathrm{NA}を自動的に省いてくれる点が普通にインデックスを作成するよりも便利である。

### subset()を用いたフィルタリング
# NAを削除する手間が省ける
x <- c(6, 1:3, NA, 12)
x[x>5]
subset(x, x>5)
2.8.3 which()関数を使ったフィルタリング

 \mathrm{which}()関数を用いてもフィルタリングできる。

# which()で特定の値の位置を返すこともできる
z <- c(5,2,-3,8)
which(z^2>8)
z[which(z^2>8)]


 またベクトル内のある条件が最初に出現する位置を見つけることもできる。

first1a <- function(x){
  for(i in 1:length(x)){
    if(x[i]==1)break
  }
  return(i)
}

first1b <- function(x)return(which(x==1)[1])

 後者の方がコンパクトではあるものの、ベクトルxすべての要素内にあるすべての1を探し無駄が多くなり得るため、一長一短である。

2.9 ベクトル化されたif-then-else

 \mathrm{R}にはベクトル化された\mathrm{ifelse}()関数がある。第一引数にブール値ベクトル、第二および第三引数にベクトルを充てる。
 ベクトル化されているため、単純なif-then-else構文よりも高速さが期待できる。

################
### ifelse() ###
################
x <- 1:10

ifelse(x%%2==0,5,12)

2.10 ベクトル等価性の検査

 \mathrm{==}では個々の成分の一致性が診断されるため、工夫が必要になる。もしくは単に\mathrm{identical}()を用いる。

x <- 1:3
y <- c(1,3,4)

x==y

all(x==y) # all()を用いることでベクトルの一致性を診断できるようになる

identical(x,y)


ただし\mathrm{identical}()は型までも一致するかを判断する。そのため、

x <- 1:3
y <- c(1,2,3)

identical(x,y)


を実行すると\mathrm{false}を返す。

2.11 ベクトルの要素名

 ベクトルの要素には\mathrm{name}()を用いて名前を付けることや呼び出したりできる。ベクトルから名前を削除するには\mathrm{NULL}を割り当てればよい。

2.12 c()に関する詳細

 連結関数\mathrm{c}()に渡す引数のモードが異なっている場合にはどのモードも含むことができるモードに変換する。

###########
### c() ###
###########

c(5,2,"abc")

c(5,2,list(a = 1, b = 4))

# c()では、2階層オブジェクトを作らない
c(5,2,c(1,5,6))
プライバシーポリシー お問い合わせ