RあるいはR言語とは、統計処理に特化したプログラミング言語のことです。回帰分析やt検定などの統計分析に加えて、webスクレイピングやメール送信なども行うことができます。
こういった作業が行えるのは、パッケージという拡張機能がユーザーによって作成・シェアされているからです。
よく比較される言語として、Pythonがあります。Pythonもまた、ユーザーが作るパッケージ等で機能拡張されています。
機械学習ではPython一強な昨今ですが、Rにも機械学習用のパッケージはあるし、最終的には自分で色々作ることになるので、基本的には「極めればRもPythonも一緒」と言われています。じゃあ、一般ユーザーとしてはどちらを使えば良いか?ということになりますが、これはやりたい作業内容や本人のスキルによるので、参考として以下に私なりにPythonと比較した際のRの長所短所を書いていきます。
がっつりプログラミングを勉強せずに統計分析がしたいならRで良いと思います。
プログラミングも勉強して、色々手広くやりたいならPythonの方が良いかもしれません。
どっちかをやっておけば、もう片方のコードもなんとなくはわかったりします。
Rの導入は
http://ryotamugiyama.com/2020/08/03/rinstall/
https://yukiyanai.github.io/jp/resources/
あたりを参考にしてみてください。以前、Windowsユーザーを中心にRの導入段階で大混乱が起きました。
特にOnedriveと同期している方々は、トラブルが起きる確率が高いのでインストール前に上記記事に目を通すことを勧めます。
関数名(引数) という形をとります。自分で関数を作ることも可能です。sum(X) です。その他にも、欠損値 NA を無視するためのオプション na.rm=TRUE など、様々な引数が関数ごとにあります。class(オブジェクト名) で調べられるので、処理がうまくいかない時にはチェックしてみましょう。OBJ <- as.double(OBJ) とすれば数値として再認識してくれます。ただし、カンマやスペースなどが入ってしまっていると、うまく数値に直せなくて欠損値 NA になってしまったりします。Rstudioでは様々なキーボードショートカットが使えて、快適なコーディングには欠かせないものになっています。
Alt + Shift + K などで確認できるので、積極的に習得しましょう。
個人的なお勧めは
Ctrl(Command) + Shift + R セクションの挿入Ctrl(Command) + Shift + O スクリプトの目次の表示Alt + - 代入演算子 <- の挿入Ctrl(Command) + Alt + R スクリプト全体の実行などです。環境によってキーに差があるかもしれません。ご了承・ご確認ください。
PCのファイルが入っている場所のことをディレクトリと呼びます。
DocumentsとかDownloadsとかもディレクトリの一つです。
ワーキングディレクトリ(以下、wd)とは、R(に限らず様々なソフト)が作業を行う場所を指します。
具体的には、デフォルトのファイルの保存先や、ファイルを探す場所などがwdとなります。
MATLAB等ではより顕著かつ分かりやすいですが、多くのソフトや言語ではファイルの名前が指定された時に、PC全体からそのファイルを探し出す、ということはしてくれません。具体的にファイルがどこにあるか(ファイルパスと呼びます)を教えるか、wdに見つけて欲しいファイルを入れておく必要があります。
自分のRがwdをどこに設定しているかは、getwd()というコマンドで確認できます。
getwd()
> [1] "/Users/KosukeA/Documents/RA/REAS"
この場合ですと、Documents内のRAフォルダの中のREASというフォルダがwdになっています。
例えばこのフォルダ内に Data.csv というファイルがあるならば、 read.csv("Data.csv") というコマンドで簡単にデータが読み込めます。
それに対して、もしDocumentsの中に Data.csv があるならば、Rがファイルの場所を理解できるように、 read.csv("/Users/KosukeA/Documents/Data.csv") としなければいけません。ファイルパスの確認方法はOSごとに異なりますが、getwd()の結果を参考にして手入力するのが楽な時も多いです。
ファイルを保存したりする場合も同様で、 何もつけずに write.csv(file,"DATA.csv") とすれば file が DATA.csv という名前でREASというフォルダに保存されますが、これをDocumentsに保存したいならば write.csv(file,"/Users/KosukeA/Documents/DATA.csv") としなくてはいけません。
いちいちこう入力するのは面倒なのに加えて、コードを間違えた時に大事なファイルを上書きしてしまうと大変なので、R用のフォルダを作り、そこをwdとして、必要なデータ等はそこにコピーして作業することを推奨しています。
余談ですが、私はRprojectという機能を使って作業内容別にwdとするフォルダを作っています。gitによるバージョン管理ができたり何かと便利ですが、ややこしいのでワードだけ紹介して省略します。
では、どうやってwdを指定するかですが、こちらは setwd("ファイルパス") で行えます。実際にDownloadsをwdにしてみましょう。
setwd("/Users/KosukeA/Downloads")
getwd()
> [1] "/Users/KosukeA/Downloads"
getwd() の結果が”/Users/KosukeA/Downloads”に変わりました。無事にwdを変更できています。
Rにはパッケージと呼ばれる拡張機能のようなものがたくさん存在し、それをインストールすることで様々なことができるようになります。
パッケージのインストール方法は色々とありますが、Rstudioでは画面上のメニューから Tools > Install Packages をするのが一番楽なのではないかと思います。
ただし、この方法でインストールできるのは CRAN と呼ばれるサイトに含まれるメジャーなものだけなので、マイナーなパッケージを使いたい時は自分で別途入手手段を探さないといけません。
インストールしたパッケージは、 library(パッケージ) というコマンドを使うか、 パッケージ名::関数名 とすることで使うことができます。
基本的には前者を使う方が楽ですが、複数のパッケージで共通の名前を持つ関数が存在する場合、後者の書き方をする必要が出てきたりします。
例えば lag はパッケージ plm と dplyr に含まれているので、dplyr の lag を確実に使いたい時は dplyr::lag() とします。
ちなみに後で紹介する %>% も tidyverse というパッケージなどに含まれる拡張機能です。
後々使うのでここで有効化しておきましょう。
library(tidyverse)
Rでは行った作業の結果に名前をつけて、Rの中で繰り返し使用することができます。別途保存作業を行わない限り、PC内にはデータとして保存されません。
こうしてR内に保存された数列やデータ表などは、グローバル変数と呼ばれます。Rstudioでは右上のGlobal Environment で一覧を確認できます。
名前の付け方としては一般的に、 付けたい名前 <- 保存したい作業 という書き方をします。
(付けたい名前 = 保存したい作業 でも大体の場合うまくいきますが、一部の作業では代用できません。)
例えば、まだ何も保存していない状態で、 DATA と打ってもエラーが出ますが、
DATA
> Error in try(DATA) : オブジェクト 'DATA' がありません
DATA <- 1を実行してから DATA と打つと、DATAの中身である1を返してくれます。
DATA <- 1
DATA
> [1] 1
プログラミング言語では、繰り返し処理として for が使われます。
Rでは、 for ( i in vector ) {...} というような書き方をします。この時、 i に vector の中身を一つずつ順番に代入して、 {…} という処理を繰り返し行うことになります。具体例を一つ書いてみましょう。
for (i in 1:3){
print(i)
}
> [1] 1
> [1] 2
> [1] 3
ここで、1:3 は1から3までの1ずつ増える数列です。5から8が良ければ 5:8 と書きます。
また、print(i) は i を画面に出力するというコマンドです。
結果として、「iを画面に出力する」という動作を、「iに1,2,3を順番に代入して」行うことができました。
また、大体のプログラミングでは if で条件を指定して動作を行うことが多いです。
Rでは if ( CONDITION ) {...} みたいな書き方をします。CONDITIONが満たされるなら {…} を実行する、といった具合です。 ここでは有名なFizz Buzz問題を for と if を使って解いてみましょう。Fizz Buzz問題とは、
というものです。例えば1から15の数列(Rだと1:15)にFizz Buzzを適用するなら、正解は
“1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,Fizz Buzz”
となります。
まずは1から15の数列にtestという名前を付けておきます。
test <- 1:15
では、ファーストステップとして、3の倍数をFizzに置き換えてみましょう。
という考え方でコードにしていきます。順に、
for (i in 1:15)if (i%%3==0)test[i] <- "Fizz"という書き方をします。ここで二列目の i%%3 は i を 3 で割った余りを計算し、
test[i] はtestの中身のうち i 番目を指します。これに <- "Fizz" でFizzという文字を入れているわけです。
なお、文字列は”“で囲わないと、その名前のグローバル変数だと解釈されるので、DATAの時と同じように、そんなオブジェクトはないと怒られます。
実行して結果を見てみましょう。
for (i in 1:15) {
if (i %% 3 == 0) {
test[i] <- "Fizz"
}
}
test
> [1] "1" "2" "Fizz" "4" "5" "Fizz" "7" "8" "Fizz" "10"
> [11] "11" "Fizz" "13" "14" "Fizz"
これを5の倍数、15の倍数についても同じようにすれば、Fizz Buzz問題はクリアです。
置き換え作業をするだけでこんなにコードを書くのか、と思う方もいるかもしれませんが、短く書くこともできるのでご安心ください。Fizz Buzzも存外奥が深いんです。
map_chr(1:15,~{if (.%%15==0) "Fizz Buzz" else if (.%%5==0) "Buzz" else if (.%%3==0) "Fizz" else .})
> [1] "1" "2" "Fizz" "4" "Buzz" "Fizz"
> [7] "7" "8" "Fizz" "Buzz" "11" "Fizz"
> [13] "13" "14" "Fizz Buzz"
A=B なら処理1を、そうでないなら処理2を実行したい場合は、 else を使います。
1から4の数字について、奇数なら Odd 偶数なら Even と表示するコードを書いてみましょう。
for (i in 1:4){
if (i%%2==1) {
print("Odd")
} else {
print("Even")
}
}
> [1] "Odd"
> [1] "Even"
> [1] "Odd"
> [1] "Even"
ところで { } は、複数行にわたる処理を一塊のものとして扱う際に使います。
条件を満たした時に実行して欲しい処理が複数ある場合などは、どこからどこまで実行して欲しいかの明文化が必要です。
ところで if の中では == を等号として使っていました。イコールが2つ並んでいる場合は、「等号が成立するか否か」が返されます。
こういった場合に返される、命題が真か偽かを示す値を、論理値(logical value) と呼びます。
== 以外にも例えば、
!= 等号が成立しないかどうか%in% 含まれるかどうか!A%in%B AがBに含まれないかどうかis.numeric() 数字かどうかなど、論理値を返すものはたくさんあります。返り値は常に TRUE か FALSE です。
"Doraemon"=="Nobita"
> [1] FALSE
"Doraemon"!="Nobita"
> [1] TRUE
6%in%1:15
> [1] TRUE
!6%in%1:15
> [1] FALSE
is.numeric("Doraemon")
> [1] FALSE
また、論理和 (A or B) は | 、論理積 (A and B) は & を使います。
1+2==3 | 1+3==3
> [1] TRUE
1+2==3 & 1+3==3
> [1] FALSE
ところで、 ベクトル == 数値 を実行すると、「ベクトルの各要素が数値と一致するか」を要素ごとに判定して、TRUE/FALSEのベクトルを返します。
(ベクトル == ベクトル だと、第i成分が一致するかどうかの判定を各iで判定して、やはりTRUE/FALSEのベクトルが返ってきます。)
この応用で、 Vector1[Vector2 == 数値] などを実行して、Vector1の成分のうち、Vector2 = 数値 が成立するインデックスに位置するものだけを抽出することができます。
例として、以下の2つのベクトルを使って見てみましょう。
Vector1 Vector2
1 1
2 2
3 3
4 1
5 2
6 3
7 1
8 2
9 3
10 1
Vector2 = 1 が成立する行にある、Vector1 の 値を抽出します。
Vector1[Vector2==1]
> [1] 1 4 7 10
こういった作業は、例えば西暦2015年以降は1になるようなダミー変数を作りたい時など、何か指標に基づいて別の指標に手を加えたり、別の指標を作成したりしたい時に便利になってきます。
なお、論理値に関して細かいことを言うと、if はかっこの中が TRUE なら実行する、という仕組みになっています。
こういった仕組みをたまに活かして行うのがwhileループです。
一定の条件が満たされている限り動作を繰り返す、という処理を while を使って行うことができます。
while ( CONDITION ) {...} で CONDITION が満たされている( = TRUEである)間、 {…} が繰り返されます。
for では繰り返し作業の回数が vector の要素の個数で決まりますが、while はコードを書いた時点では何回繰り返すか決まっていません。
そのため、一定の水準を満たすまで何回もトライするような場合によく使われます。
例として、厚さ0.09mmのコピー用紙を何回折ったら月に届くか while を使って計算してみましょう。
月までの距離は384400000000mmで、紙は1回折るごとに厚さ thickness が2倍になります。
計算方法として、紙の厚さが月までの距離より小さい限り、紙を折って厚さを2倍にするのを繰り返すことにします。
折った回数は、whileループをする度に count を1ずつ増やすことでカウントします。
thickness <- 0.09
count <- 0
while(thickness<384400000000){
count <- count + 1
thickness <- thickness*2
}
print(count)
> [1] 42
print(thickness)
> [1] 395824185999
42回で月までの距離を超えることを確認できました。
なお、手動で止めるまで動作を繰り返し続けて欲しい場合などは、 while(TRUE) とすることで無限ループになり、自動化等ができます。 タイポなどで上のコードを thicknes <- thickness*2 とかにしてしまった場合も、永遠に thickness<384400000000 が TRUE なのでやはり無限ループします。処理が終わらない時は無限ループになっていないかチェックしましょう。
for, while ループでは、 if と併せて next , break というコマンドが使えます。
next が実行されると、その時点でそのループは終了し、次のループへと移ります。
break が実行されるとその時点で全てのループが終了し、for / while ループから抜けます。
例えば以下の2つのコードを見比べてみましょう。
for (i in 1:3){
if (i == 2 ) next
print(i)
}
for (i in 1:3){
if (i == 2 ) break
print(i)
}
上の方のコードでは、 i=2 の時、 print(i) を実行する前に next が機能するので、 print(2) は実行されず、1と3だけが画面に表示されます。
for (i in 1:3){
if (i == 2 ) next
print(i)
}
> [1] 1
> [1] 3
一方、下の方のコードでは、i=2 の時、 print(i) を実行する前に break が機能するので、 print(1) だけ実行して、ループは終わってしまいます。
この場合、実は終わった時の i の値も2になっていて、「 i に3を代入する」という for ループの処理自体が行われていないことがわかります。
for (i in 1:3){
if (i == 2 ) break
print(i)
}
> [1] 1
グラフは ggplot2 というパッケージを使って描くことが多いです。このパッケージは tidyverse パッケージに含まれているので、 library(tidyverse) とした時点で実は既に使えるようになっています。
書き方の基本は、
ggplot(データ名)+
aes(x=X軸,y=Y軸)+
geom_point()+ # 散布図ならこれ
geom_bar()+ #棒グラフならこれ
geom_line()+ #折れ線グラフならこれ
theme() #ここでフォントなどを調整する
です。 geom~ は書きたいグラフに応じて使い分けてください。 theme はなくても動きます。
ここでは前節で計算した紙の厚さと折った回数の関係と、月までの距離をグラフで表現してみましょう。
最初に、紙を折った回数と紙の厚さを、データの形(dataframe)にまとめます。
データフレームの作り方も色々とありますが、ここでは一番シンプルな data.frame 関数を使います。
data.fram(列名 = 列の中身,・・・) という書き方をします。
tmp <- data.frame(ct=0:42,thick=0.09*2^(0:42))
この tmp を使ってグラフを書きましょう。
ここでは更に、 geom_hline() を使っています。 hline は horizontal line の略です、多分。横線を引くことができます。
縦線はおそらく vertical line の略で geom_vline() です。
hline の場合は yintercept で、 vline の場合は xintercept で、どこに線を引くのか指定できます。
今回は月までの距離で yintercept を指定しましょう。
実線が2本だとややこしいかもしれないので、更に linetype= で、 geom_hline を破線にしています。
その他にも、 color= や alpha= などで、色や透明度を変えたりもできます。
ggplot(tmp)+
aes(x=ct,y=thick)+
geom_line()+
geom_point()+
geom_hline(yintercept = 384400000000,linetype="dashed")
ちゃんと42回目で初めて月までの距離を超えていますね。
ところで、今作った tmp というデータフレームには ct と thick という列が含まれていましたが、これらの列をRで単純に ct や thick と入力して呼び出すことはできません。列名は、先述のグローバル変数に含まれないからです。ややこしく感じるかもしれませんが、グローバル変数に含まれないおかげで、同じ列名を持つ異なる名前のデータフレームを作成でき、それらのデータフレーム同士で、共通の名前の列が干渉しあうことも起こりません。
(グローバル変数では、同じ名前のものを2つ作ろうとしても、先に作った方は後に作った方に上書きされてしまいます。)
データフレーム等の中にあるオブジェクト・列・ベクトルを呼び出す際には、 データフレーム名$列名 あるいは データフレーム名["列名"] と入力します。後者はダブルクォーテーションで囲まないと、 列名 というグローバル変数だと認識されてエラーが出たりします。
Rでも他の言語でも、最初のうちはこの、「グローバル変数」と「その他のオブジェクト」の区別が非常にややこしく、ストレスフルだと思います。
「オブジェクトが〜」というエラーメッセージが出た際には、自分の扱いたいオブジェクトがグローバルか否かを再考すると解決することが多いです。
tidyverse や dplyr 、 rvest など特定のパッケージを library() で有効化している場合、パイプオペレーターと呼ばれる %>% を使うことができます。
Ctrl + Shift + M や Command + Shift + M で入力できます。
これは、 ‘%>%’ の左にあるオブジェクトを、右にある関数の引数として扱う、という意味になります。
例えば、最初の方に使った print() を使って画面に “Hello World!” と表示したい場合、
print("Hello World!") と "Hello World!" %>% print() のどちらでも目的を果たすことができます。
print("Hello World!")
> [1] "Hello World!"
"Hello World!" %>% print()
> [1] "Hello World!"
これだけでは %>% を使うメリットは分かりませんが、実際の作業で多くの処理を一つのオブジェクトに対して行う場合、コードの煩雑化を避けるのに大いに役立ちます。
私が実際に研究で用いたコードについて、 %>% を使った場合と使わなかった場合の例を以下に書いてみます。
このコードでは、 abroad1 というファイルを読み込んで、形を扱いやすいものに変え、変な記法がされていた年データを直し(日本のデータは和暦の存在もあり、年を表す部分の修正が必要になる場合が非常に多いです)、年と産業の名前ごとに売上の合計を計算し、その合計の一覧に abs と名付ける、という作業を行なっています。
# %>% を使わない例 1
abs <- read.xlsx("abroad1.xlsx",sheet="sales")[,-1]
abs <- pivot_longer(data = abs,cols = -name,values_to = "sales",names_to = "year")
abs <- rowwise(abs)
abs <- mutate(abs,year = as.numeric(paste0(unlist(str_extract_all(year,pattern = "\\d")),collapse = ""))+2000)
abs <- group_by(abs,year,name)
abs <- mutate(abs,sales=sum(sales,na.rm = T))
abs <- slice_head(abs)
abs <- ungroup(abs)
# %>% を使わない例 2
abs <- ungroup(slice_head(mutate(group_by(mutate(rowwise(pivot_longer(data = read.xlsx("abroad1.xlsx",sheet="sales")[,-1],cols = -name,values_to = "sales",names_to = "year")),year = as.numeric(paste0(unlist(str_extract_all(year,pattern = "\\d")),collapse = ""))+2000),year,name),sales=sum(sales,na.rm = T))))
# %>% を使って書いたコード
abs <- read.xlsx("abroad1.xlsx",sheet="sales")[,-1] %>%
pivot_longer(-name,values_to = "sales",names_to = "year") %>%
rowwise() %>%
mutate(year = str_extract_all(year,pattern = "\\d") %>%
unlist() %>%
paste0(.,collapse = "") %>%
as.numeric()+2000) %>%
group_by(year,name) %>%
mutate(sales=sum(sales,na.rm = T)) %>%
slice_head() %>% ungroup()
好みは分かれると思います。特に1つ目は、1行につき1つの作業しかしていないので、見る分には分かりやすいです。 ただ、書くのはなかなか面倒で、 abs <- が連なっているのは代名詞のない作文みたいで冗長に思えます。
2つ目は作業を全てまとめて行っているので、括弧の数がとにかく多くて、書く分にも見る分にもややこしくなっています。括弧の数が合わずにエラーが出る可能性が高いし、どの関数が何をどうしているか見るのに手間がかかります。
3つ目の書き方では、作業を全てまとめて行いつつも、作業ごとに %>% で区切ってあるので、関数とその引数が分かりやすいかと思います。 abs も省略できるので、括弧の中に何も入力しなくても rowwise() などがちゃんと動作します。反面、 %>% を使ったことのない人から見ると最初は理解が難しいと思います。
Rを本格的に使っていくなら、3番目の書き方に慣れておくことをお勧めします。職場のRユーザーなら、 %>% にも慣れていると思うのでチェックしてもらう分にも安心でしょう。
余談ですが、 %>% を使う際には関数の括弧の中身を省略できますが、何も書かないという省略方法の他に、 . で代用するという方法もあります。こちらは、明確にどこに %>% の左側のオブジェクトが入って欲しいかを示すことができます。 print の例に戻るならば、
"Hello World!" %>% print(.)
> [1] "Hello World!"
でも良いということです。
Rでは自分で関数を作ることができます。作り方としては
関数の名前 <- function(関数の引数){関数の処理内容・return(返して欲しい値)} です。
ここでは、平均を求める関数 mymeanfunc を作りたいと思います。勿論、平均を求める関数はすでにRに存在しますが、ここではオプションで算術平均と幾何平均のどちらを求めるか選べるようにしてみましょう。
N個の数字の平均を求める時、 \[算術平均=\frac{\sum_{n=1}^N X_n}{N}\qquad ,\qquad 幾何平均 = \prod_{n=1}^N (X_n)^{\frac{1}{N}}\] です。
mymeanfunc <- function(X,option="Arithmetic"){
if (option == "Arithmetic") {
# length は ベクトルの長さ i.e. 要素の数を返す関数です
return(sum(X)/length(X))
}
if (option == "Geometric") {
# prod は ベクトルの product (積。全部かけ合わせた値) を返します。
# 今回は X^(1/length(X)) で X の 1/N 乗 をかけ合わせています。
return(prod(X^(1/length(X))))
}
}
これで mymeanfunc(X=平均を求めたいベクトル) とすれば、平均を求めることができます。
上のコードで function の中に option="Arithmetic" と書いてありますが、これは
「option という引数について何も指定がなかったら “Arithmetic” を使う」という意味になります。
言うならば、デフォルトの引数ですね。
動作を確認してみましょう。
mymeanfunc(1:10)
> [1] 5.5
mymeanfunc(1:10,option = "Arithmetic")
> [1] 5.5
mymeanfunc(1:10,option = "Geometric")
> [1] 4.528729
大丈夫そうです。