歩いたら休め

なんでこんな模様をしているのですか?

【R】ふつうのスクリプト言語プログラマーのためのR言語入門

「他のスクリプト言語ならそれなりに触れるけど、Rって変な機能が多いから難しい」と感じている人のために、Rの独特な点、ハマりどころ、そして特にデータの集計での強力な機能やパッケージをまとめてみました。

社内のデータリソース移行の作業の中で、前任者が(良い意味でも悪い意味でも)すごいRプログラマーだったのと、DBからのデータの取得からデータの整形・確認が一気に行えるという理由で久しぶりにRを触りました。

(先輩!僕が「%>%ってなんですか?」みたいな質問すると、チャットでgithubのリンクを貼られたり、AdvancedRを薦められたりするの、めっちゃ困りました!)

数年前Rを使っていたときは、

  • Rのプログラムってよくわからないなあ…、後から自分のコード見ても意味が読み取れないよ…
  • Pythonでpandasとかmatplotlibってパッケージを使えばRっぽいことができるの?じゃあそっち使おっと!

と思っていたのですが、いくつかのパッケージを利用することで、(PythonやRubyのような)スクリプト言語ユーザーにとっても、わかりやすいRのコードが書けることに気づきました。それだけでなく、Rの強力な集計機能やパイプ演算子%>%と組み合わせ、楽しいプログラミングができるようになりました。

Rの機能は強力である分、慣れないと書き方が分からない、一応書けるけど非効率な書き方をしてしまうということがあると思います。 Rってよく分からないなあって思っている方に、少しでもお役に立てれば幸いだと思い、 私がこの三ヶ月分くらいの間に学んだことを共有したいと思います。

R言語の特徴

Excelせよ他のプログラミングにせよ、データの集計や分析は、だいたい以下の様な段階に分けられると思います。

  1. データの読み取り
  2. データの加工・集計
  3. (モデル化)
  4. データの書き出し・可視化

まずExcelについて考えると、まず、「データの読み取り」の段階では.xlsxや.csvなどの形式なら読み込めますが、jsonを読み込むのは難しそうです。 「データの加工」は問題なくできますが、一度行った作業を何度も繰り返すためにはマクロやVBAを使う必要があり、 プログラミング言語としては少し面倒です。

Pythonなどの汎用プログラミング言語を考えると、データの読み取りは簡単にできますが、 データの加工や集計は少々面倒ですし、グラフを使って可視化するには特別なライブラリ(Pythonならmatplotlibなど)が必要だったりします。

これを踏まえると、Rではデータ解析のプロセスを全部一つの言語でこなせるようになっており、 特に強みは次の3つにまとめられると思います。

  1. データの加工が圧倒的に楽(dplyr, tidyr)
  2. データの可視化が高機能
    • 標準のplot関数で大体のデータは可視化でき、ggplot2等でおしゃれなプロットも作れる
  3. 最新の統計モデルがライブラリの形ですぐに公開される

他の言語から来た人は、標準でグラフをプロットする万能な関数(plot()、ヒストグラムもhist()だけで書けます)があったり、R自体にパッケージをインストールする関数(install.package())があることに驚くかもしれません。

そのため、「Rはプログラミング言語ではなく統計のための開発&実行環境である」のように言われることもあります。

Rの情報源・入門書

まずは、Rの基本的な使い方を説明します。 とはいっても、紙面の都合上、他言語のプログラマーにとって違和感のあるところ、つまづきやすいところしか紹介しません。 一度Rの本を読んでみましょう。

同僚のRプログラマーに「何かいい入門書はないか」と質問したところ、 『新米探偵、データ分析に挑む』という本を教えてもらいました。 dplyrなどのライブラリを使った、今までのRの本よりわかりやすい(最近の主流の)書き方が紹介されています。

新米探偵、データ分析に挑む

新米探偵、データ分析に挑む

Rの開発環境はRStudioがおすすめです。 インストール方法などはこの記事では紹介しないので、下のサイトを参考にしてください。

markezine.jp

また、(他の一文字の名前の言語でも同じだと思いますが)RはGoogleで検索しづらいです。 Rのための検索エンジンのseekRというものがあるので、わからない点があればここで調べましょう。

seekr.jp

また、関数の仕様がよくわからないときは、help関数が便利です。 例えばprint関数に対して実行すると、

help(print)

このようにドキュメントが表示されます。

f:id:takeshi0406:20160126233609p:plain

使ってみよう!

まず、RStudioを開いてコンソールに次のように打ち込んでみましょう。

1 + 1
# [1] 2

2という値が返ってきました。RのREPLは電卓として便利です。飲み会のワリカンの計算にも使いましょう。

次に、irisと打ち込んでみましょう。Rにはいろいろなテストデータがいくつか最初に入っていて、「この関数ってどういう動きをするんだっけ?」って悩んだときに、コンソールを使って確かめることができます。

iris
#     Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
# 1            5.1         3.5          1.4         0.2     setosa
# 2            4.9         3.0          1.4         0.2     setosa
# 3            4.7         3.2          1.3         0.2     setosa
# 4            4.6         3.1          1.5         0.2     setosa
# 以下略

150行あるデータが出てきます。これはRのデータフレームと呼ばれる型で、データベースのテーブルのような操作が簡単にできます。このデータフレームの操作によってデータの集計が簡単にできるのがRの最大の強みです。特に後で紹介するdplyrパッケージを使うと最強です。

ちなみにこのirisデータは、Fisherという偉い学者さんの研究で使われたデータセットらしいです。

各列(カラム)のデータだけを見るには、$を使って

iris$Sepal.Length
#   [1] 5.1 4.9 4.7 4.6 5.0 5.4 4.6 5.0 4.4 4.9 5.4 4.8 4.8 4.3 5.8 5.7 5.4 5.1 5.7 5.1 5.4 5.1 4.6 5.1 4.8
#  [26] 5.0 5.0 5.2 5.2 4.7 4.8 5.4 5.2 5.5 4.9 5.0 5.5 4.9 4.4 5.1 5.0 4.5 4.4 5.0 5.1 4.8 5.1 4.6 5.3 5.0
#  [51] 7.0 6.4 6.9 5.5 6.5 5.7 6.3 4.9 6.6 5.2 5.0 5.9 6.0 6.1 5.6 6.7 5.6 5.8 6.2 5.6 5.9 6.1 6.3 6.1 6.4
#  [76] 6.6 6.8 6.7 6.0 5.7 5.5 5.5 5.8 6.0 5.4 6.0 6.7 6.3 5.6 5.5 5.5 6.1 5.8 5.0 5.6 5.7 5.7 6.2 5.1 5.7
# [101] 6.3 5.8 7.1 6.3 6.5 7.6 4.9 7.3 6.7 7.2 6.5 6.4 6.8 5.7 5.8 6.4 6.5 7.7 7.7 6.0 6.9 5.6 7.7 6.3 6.7
# [126] 7.2 6.2 6.1 6.4 7.2 7.4 7.9 6.4 6.3 6.1 7.7 6.3 6.4 6.0 6.9 6.7 6.9 5.8 6.8 6.7 6.7 6.3 6.5 6.2 5.9

値を取ることができました。ちなみにSepal.Lengthという変数名に驚いた方もいると思いますが、Rでは変数名にドット"."が使えます。不思議ちゃんですね。

また、[]を使って次のようにカラムを取り出すこともできます。

iris['Sepal.Length']
#     Sepal.Length
# 1            5.1
# 2            4.9
# 3            4.7
# (以下略)

$の場合と出力形式が少し違いますが、これは$のときはカラムの中にあるベクトル(ベクトルについては後で説明します)で取り出されるのに対し、[]を使ったときはSepal.Lengthのカラムだけのデータフレームで取り出されます。

Rのプログラムは、ほとんどの場合、csvやデータベースの値をデータフレームの変数に格納して利用します。 例えば、csvのデータを読み込むにはread.csvという関数を使います。

example_df <- read.csv('./example.csv')

http://cse.naro.affrc.go.jp/takezawa/r-tips/r/40.html

違和感1:変数の代入が"<-"

for文やif文など、基本的な構文を紹介していってもいいのですが、他の言語をやった人ならほとんど詰まることはないと思います。 ただし、Rには変な構文が多いので、違和感あると思うところをいくつかピックアップしてみます。

まず、変数の代入が、=でなく<-ということ。また、->で左から右に代入できました。でも、実は=でも代入できてしまいます。不思議ちゃんですね。

x <- 'hello R!'
x
# [1] "hello R!"

# 実は=でも代入できるが非推奨
x = 'hello R!'
x
# [1] "hello R!"

# 実はこれでも左から右に代入できる!
1 + 1 -> y
y
# [1] 2

また、関数は関数を代入する形で宣言します。JavaScriptの関数式みたいな感じです。

add <- function(x, y) {
  return (x + y)
}

add(1, 2)
# 3

# 明示的にreturnを書かなくても大丈夫です
add <- function(x, y) {x + y}

違和感2:ベクトルとリスト

また、配列っぽいデータ構造にベクトルとリストという二種類があります。

c(1, 2, 3)
list(1, 2, 'a')

この2つはどう違うのでしょうか?実は、ベクトルは「同じ型の値が入る」という制限があり、同じ操作をベクトル全体にかける操作ができます。PythonやRubyのmapみたいなイメージです。これによって数値計算やデータ解析がめちゃめちゃ楽になります。

# ベクトル全体に1を足す
c(1, 2, 3) + 1
# [1] 2 3 4

# ベクトルの各要素を足し上げる
c(1, 2, 3) + c(10, 20, 30)
# [1] 11 22 33

他にも、数学の内積とかその辺の操作もできます。

このため、Rではほとんどfor文を書くことはなく、ベクトル演算で操作することが推奨されています。 Rのfor文は他言語に比べても遅いそうですし。

Rの多くの関数は、ベクトルに対して一気に操作できます。

# 苗字を取る操作を関数をまとめる
get_family_name <- function(name) {
  # スペースの位置を切り出す' '
  name_seq <- regexpr(" ", name)
  # 苗字を切り出す
  family_name <- substr(name, 1, name_seq - 1)
  return (family_name)
}

# 苗字を取得する
get_family_name('hoshimiya ichigo')
# [1] "hoshimiya"

# ベクトルに対して一気に操作できる
get_family_name(c('hoshimiya ichigo', 'kiriya aoi', 'shibuki ran'))
# [1] "hoshimiya" "kiriya"    "shibuki" 

実は、数値計算ライブラリであるnumpyを使えば、Pythonでも同じようなベクトル型を利用することができます。 Rubyにもありそうな気がしますが、詳しくは知りません…。

また、文字列と数字を混ぜてベクトルを宣言するとどうなるのでしょうか?

c(1, 2, 'a')
# [1] "1" "2" "a"

エラーを発せず、全部文字列型に変換されてしまいました。 Rでは「表現力の強い型に変換される」というルールで暗黙の型変換が行われ、 例えば文字列と整数では文字列に、整数と実数では実数に変換されます。 (比較演算==でも同様なので要注意です)。

人によってはPHPのトラウマが呼び起こされると思いますが、 型についてほとんど考えなくて良いので、例えばcsvから読み込んだデータをアドホックな分析をする際には楽です。 (逆に、厳密に例外処理したシステムを作ることには向いてないかもしれません)。

一方、リストは型を混ぜて要素を入れることができます。

list(1, 2, 'a')
# [[1]]
# [1] 1
#
# [[2]]
# [1] 2
# 
# [[3]]
# [1] "a"

また、リストの各要素に名前をつけることもでき、連想配列のように使うこともできます。

aikatsu <- list(soleil = c('ichigo', 'aoi', 'ran'), powa2puririn = c('otome', 'sakura', 'shion'))
aikatsu$soleil
# [1] "ichigo" "aoi"    "ran"   

ただし、キーがユニークであるという保証はありません。

aikatsu <- list(soleil = c('ichigo', 'aoi', 'ran'), soleil = c('hoshimiya', 'kiriya', 'shibuki'))
aikatsu
# $soleil
# [1] "ichigo" "aoi"    "ran"   
# 
# $soleil
# [1] "hoshimiya" "kiriya"    "shibuki"  

# `$`では最初の要素にしかアクセスできない
aikatsu$soleil
# [1] "ichigo" "aoi"    "ran"   

実は、これはRubyのハッシュやPythonの辞書とは異なり、listのルックアップという機能で計算が遅いそうです。 一応、hashというライブラリでRubyのハッシュみたいなものが使えるようになるようですが。

qiita.com

そうそう、一つ大事なことをいい忘れていました。 Rのリストやベクトルのシーケンスは、ほとんどの言語と異なり「0」ではなく「1」から始まります。

a <- c(1, 2, 3)
a[1]
# [1] 1

a[2]
#[1] 2

a[0]
# numeric(0)

これは、Rが、C言語ではなく数値計算用のFortranから強く影響を受けたからだそうです。 他にも、数値計算言語のMATLABやJuliaでも配列は1から始まります。 たしかに、数学で「第一成分」「第二成分」と数えているものを素直にコードに落とし込めるのですが、 他の言語との対応を考えるとき(例えばRのコードをPythonのnumpyに移植したい場合など)には面倒です。

さらにベクトルやリストについて詳しい話は、こちらのサイトを参照してください。 http://cse.naro.affrc.go.jp/takezawa/r-tips/r/23.html

実は、先ほど話に出たデータフレームは、「ベクトルに入ったリスト」だそうです(R言語の名著『Advanced R』に書いてました)。 そのため、先ほどベクトルの話で出たように、「データフレームの各カラム(ベクトル全体)に対して同じ操作を行う」という操作も簡単に行うことができます。

Advanced Rは今度日本語訳が出るそうなので楽しみですね。

R言語徹底解説

R言語徹底解説

違和感3: 全てが「データ」と「関数」

Rの世界は、データと関数で構成されています。 他の言語で「文」の機能のほとんど(おそらく全て)が、関数として実装されています。 例外処理もtry関数もしくはtryCatch関数を使います。

d.hatena.ne.jp

例外処理 | Rのtry関数とtryCatch関数による例外処理

三項演算子のようなものも、ifelse関数を使います。

x <- NA # NAは「未定義」という意味
ifelse(is.na(x), 0, x)
# [1] 0

x <- 1
ifelse(is.na(x), 0, x)
# [1] 1

実は、+演算子なども関数です。「`」で囲むことによって通常の関数のようにも使えます。 中置の演算子が関数としても使えるっていうのはHaskellっぽいですね。

`+`(1, 2)
# [1] 3

# 高階関数で使うこともできる。
Reduce(`+`, c(1, 2, 3))
# [1] 6

# 2引数の関数として演算子を定義することもできる
"&" <- function(chr1, chr2) { paste(chr1, chr2, sep = '') } 
'tom' & 'jerry'
# [1] "tomjerry"

また、可変長引数の関数がめちゃめちゃ多いことも特徴です。 代表的なものが、文字列結合の関数のpasteです。

# なぜかデフォルトではスペース区切りで出力される
paste('SELECT', '*', 'FROM', 'example_table')
# [1] "SELECT * FROM example_table"

paste('SELECT', '*', 'FROM', 'example_table', sep = '')
# [1] "SELECT*FROMexample_table"

# デフォルトの区切り文字が空文字の、paste0関数もある
[1] "SELECT*FROMexample_table"
# [1] "SELECT*FROMexample_table"

このように、Rは似た機能の関数が多数あったり、前身になったS言語の関数名が残っていたりするので、 少々わかりづらいところがあります。 特にほかの人にプログラムを渡すときは注意すべきでしょう。

dplyr, tidyrによるデータの整形

データの整形にはdplyrというパッケージを使いましょう。 私は以前「Rでのデータフレームの操作って難しいなあ…SQLならどう集計すればいいのかわかるのに」と思っていたのですが、 dplyrを使うことで思い通りの集計が行えるようになりました。

実は、dplyrはここ数年の間に現れたパッケージで、Rのプログラマーの間で広く使われているパッケージです。 もし昔(2〜3年前)Rに触れて、最近Japan.Rなどの勉強会に参加するようになった人がいれば、 まず発表の中で(dplyrで使われる)パイプ演算子%>%が乱舞していることに驚くと思います。

dplyrというパッケージは、次のようなものです。

これは、@matsuou1さんの素晴らしい記事があるので、まずはそちらを読んでください。

qiita.com

まずは、パイプ演算子%>%の説明をした後、dplyrの操作を説明します。

dplyrで使われるパイプ演算子の説明

関数の説明のところでも、実はRでは演算子を(関数として)簡単に定義することができるため、 ライブラリによっては便利な演算子も読み込まれることがあります。 (ただし、実はパイプ演算子はdplyrのオリジナルではなく、内部でmagrittrという別のパッケージが呼び出されているそうです。)

実際に、パイプ演算子%>%を使ってみましょう。 RStudioなら、パイプ演算子はCtrl + Shift + M(MACならcommand + Shift + M)のショートカットで入力できます。

# dplyrパッケージを読み込んでおく
library(dplyr)

# head関数は「一番目の引数(iris)の上から二番目の引数(2)行を取ってくる」という意味
head(iris, 2)
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1          5.1         3.5          1.4         0.2  setosa
# 2          4.9         3.0          1.4         0.2  setosa

# パイプ %>% を使うことで次のように書けます
iris %>% head(2)
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1          5.1         3.5          1.4         0.2  setosa
# 2          4.9         3.0          1.4         0.2  setosa


# gsubは文字列置換の関数
gsub('::DATE::', '20160126', "SELECT * FROM example WHERE log_date = ::DATE::")
# [1] "SELECT * FROM example WHERE log_date = 20160126"

# パイプで渡したい値が第一引数でない場合は、他の引数の名前を明示すればOK
"SELECT * FROM example WHERE log_date = ::DATE::" %>% 
  gsub(pattern ='::DATE::', replacement = '20160126')
# [1] "SELECT * FROM example WHERE log_date = 20160126"

# パイプで渡ってきた値を、更にパイプで渡す
"SELECT * FROM ::TABLE:: WHERE log_date = ::DATE::" %>% 
  gsub(pattern ='::DATE::', replacement = '20160126') %>% 
  gsub(pattern ='::TABLE::', replacement = 'example_log')
# [1] "SELECT * FROM example_log WHERE log_date = 20160126"

%>%は、「演算子の左の値(iris)を、左の関数の第一引数(head)に代入した値を返す」というものです。 F#の演算子から発想を得たものと聞いています。

これだけでは、素直に「head(iris, 2)って書けばいいじゃん!何が嬉しいの?」という感じですが、 シェルスクリプトやRubyのメソッドチェーンのように、次の処理を右に右に繋ぐことができます。 特に、dplyrの関数と併せて使うことで、面倒な集計を簡単に終わらせられます。

dplyrによるデータフレームの操作

@matsuou1さんの記事にあるSQLの操作との対応表がわかりやすいです。

SQL dplyr R 説明
where filter subset 行の絞り込み
count , max ,min等 summarise aggregate 集計する
group by group_by - グルーピングする
order by arrange sort , order 行を並べ替える
select select data[,c("a","b")] データフレームから指定した列のみ抽出する
(select句) mutate transform 列の追加

詳しい使い方はやはり@matsuou1さんの記事を読んで欲しいのですが、 簡単に使用感だけ説明します。

例によって、irisデータを使って説明します。

iris %>% head()
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1          5.1         3.5          1.4         0.2  setosa
# 2          4.9         3.0          1.4         0.2  setosa
# 3          4.7         3.2          1.3         0.2  setosa
# 4          4.6         3.1          1.5         0.2  setosa
# 5          5.0         3.6          1.4         0.2  setosa
# 6          5.4         3.9          1.7         0.4  setosa

あやめ(iris)の品種(Species)ごとに、花びらの長さ(Petal.Length)の平均値を出したいとき、 SQLなら次のように簡単に集計できると思います。

-- 変数名にドットがあるのは違和感あると思うので、キャメルケースにしました
SELECT species, mean(patal_width) AS mean FROM iris GROUP BY species

これをdplyrで書くと次のようになります。

library(dplyr)

iris %>% dplyr::group_by(Species) %>% dplyr::summarise(mean = mean(Petal.Length))
# Source: local data frame [3 x 2]
# 
#      Species  mean
#       (fctr) (dbl)
# 1     setosa 1.462
# 2 versicolor 4.260
# 3  virginica 5.552

# Rではライブラリ名を省略することもできる
iris %>% group_by(Species) %>% summarise(mean = mean(Petal.Length))

なんということでしょう!パイプを使って2つ関数に渡すだけで、SQLのような操作が完了してしまいました。

余談ですが、dplyrの関数はパイプを使わずに書くこともできます。 先ほどの処理は、実は

summarise(group_by(iris, Species), mean = mean(Petal.Length))

と同じ意味なのですが、素直にパイプを使ったほうが読みやすいと思います。

tidyrによるデータの変形

dplyrは、「SQLと同じ操作をプログラミング言語でも実現できる」というパッケージですが、 tidyrは「SQLでは難しい操作も簡単にできる」というパッケージです。 個人的には、Excelのピボットテーブルと使いどころが似ているのかなと思います。

まず、次のようなPV数のデータがあるとします。 それぞれの日に、サイトのPV数が都道府県ごとにいくらだったか、ということを表していると思ってください。 ちなみに、都道府県id(pref_id)の13番は東京、14番は神奈川です。

testdata <- data.frame(
  date = c('11/25', '11/25', '11/26', '11/26', '11/27'), 
  pref_id = c(13, 14, 13, 14, 13),
  pv = c(1, 2, 3, 4, 5)
)

testdata
#    date pref_id pv
# 1 11/25      13  1
# 2 11/25      14  2
# 3 11/26      13  3
# 4 11/26      14  4
# 5 11/27      13  5

アクセス解析ツールのデータやアクセスログから、ここまで集計するのはSQLでもdplyrでも簡単だと思います。 しかし、次に集計したい形は、次のようなものになると思います。

date 東京のPV数 神奈川のPV数
11/25 1 2
11/26 3 4
11/26 5 0

SQLで行う場合、次のようになると思いますが、県の数が増えた場合、似たようなカラムを何度も書く必要があって面倒です。 DBの値をちょっと確認するのにも、DBクライアントからcsvをエクスポートし、Excelのピボットテーブルで集計するような作業が入ってしまうかもしれません。

SELECT
  date,
  SUM(CASE WHEN pref_id = 13 THEN pv ELSE 0 END)) AS Tokyo_pv,
  SUM(CASE WHEN pref_id = 14 THEN pv ELSE 0 END)) AS Kanagawa_pv
FROM
  testdata
GROUP BY
  date

このような集計は、tidyr::spreadという関数で簡単に行えます。

library(tidyr)

testdata %>% tidyr::spread(key = pref_id, value = pv)
#    date 13 14
# 1 11/25  1  2
# 2 11/26  3  4
# 3 11/27  5 NA

# デフォルトでは値が入らないところにNA(未定義値)が入るため、0で補完する場合は次のように書く
testdata %>% tidyr::spread(key = pref_id, value = pv, fill = 0)
#    date 13 14
# 1 11/25  1  2
# 2 11/26  3  4
# 3 11/27  5  0

tidyrの詳しい使い方や他の関数を知りたい方は、次の記事がおすすめです。

{tidyr}でよく使う関数のメモ

purrrによる関数型プログラミング

パイプによるメソッドチェーンを覚えると、リストやベクトルに対してRubyのmapメソッドのようなことがやりたくなると思います。

こちらの記事にある、 qiita.com

  1. [2,4,6,8,10]という配列を用意して
  2. ↑の配列から8以下の数だけを選択した配列を作る
  3. ↑の配列から各要素を2乗した配列を作る
  4. ↑の配列から20以上の数だけを選択した配列を作る
  5. ↑の配列の値を掛け合わせる

という操作をR + purrrで行ってみようと思います。

library(purrr)

c(2, 4, 6, 8, 10) %>% 
  keep(~. <= 8) %>% 
  map(~. ** 2) %>% 
  keep(~. >= 20) %>% 
  reduce(`*`)

Rubyのコードと比べると対応関係がわかると思います。 purrrの内部でもdplyrと同様にmagrittrが呼び出され、パイプ演算子が呼び出せるようになります。

#rubyの場合
p [2,4,6,8,10]
  .select{|num| num <= 8; } # 1. ↑の配列から8以下の数だけを選択した配列を作る
  .map{|num| num**2 }       # 2. ↑の配列から各要素を2乗した配列を作る
  .select{|num| num >= 20 } # 3. ↑の配列から20以上の数だけを選択した配列を作る
  .reduce(:*)               # 4. ↑の配列の値を掛け合わせる

実は、map, reduceなどの高階関数と同じ機能は、 Rにもデフォルトでapply系の関数や Map, Reduce, Filterなどの関数として用意されています。 purrrでは、パイプ演算子でメソッドチェーンを作りやすいよう、それらの機能が整理されています。

例えば、デフォルトのRで先ほどのものと同じ処理を書くと、次のようになります。 実際はapply系の関数を使う人のほうが多いと思いますが、 他言語の人が見ても分からないと思うのでMapなどの一般的な名前の関数を使いました。

Reduce(
  `*`,
  Filter(
    function(x) {x >= 20},
    Map(
      function(x) {x ** 2},
      Filter(
        function(x) {x <= 9},
        c(2, 4, 6, 8, 10)
      )
    )
  )
)

まず、Rの高階関数は第一引数が関数のものが多いので、パイプ演算子%>%で処理を繋げるのには向いていません。 また、ラムダ式を書くのにいちいちfunction(){}と書くのは少々面倒です。

そのためpurrrでは第一引数が操作したいベクトルに変わっており、 第二引数にチルダ~で始まるラムダ式(~. <= 8など)を使うことができます。 この~で始まるものはRのモデル式というもので、 purrrの関数の内部で、通常の関数に展開されています。 また、普通の関数を与えたときも思った通りに動作します。

purrrのもう一つの素晴らしい点は、部分適用したい引数を、mapやreduceなどの引数として与えられることです。

c('ichigo', 'aoi', 'ran') %>% purrr::map(paste, 'chan')
# [[1]]
# [1] "ichigo chan"
# 
# [[2]]
# [1] "aoi chan"
# 
# [[3]]
# [1] "ran chan"

# ラムダ式を使って書くとこんな感じ
c('ichigo', 'aoi', 'ran') %>% purrr::map(~paste(., 'chan'))

# まあ、この場合はベクトル演算でもできるんだけどね…
paste(c('ichigo', 'aoi', 'ran'), 'chan'))

# 本当は文字列のベクトルをカンマ区切りに変換する例を出したかったのですが、
c('1', '2', '3') %>% purrr::reduce(paste, sep=',')
# 自前のMac環境では部分適用がうまくいっていないようです
# [1] "1 2 3"
# 会社のwindows環境では想定通り動いたのですが…
# [1] "1,2,3"

また、この間は、データベースに入っていない日付が何日かあったため、DBに入っていない日付をフィルターするのに使いました。 余談ですが、filterではなくkeepという関数名を使っているのは、 既にdplyrにfilterという関数名があり、混乱を避けるためだと思われます。

# データベースから取ってきた日付を、unique関数で一意にする
dates_in_db <- db_dataframe$date %>% unique()

# 調べたい日付の範囲
# 実際には一年分くらいの日付を関数で作りました
target_dates <- list('20151201', '20151202', '20151203')

# データベースに存在しない日付をフィルターする
target_dates %>% purrr::keep(~!. %in% dates_in_db)

さらにpurrrについて詳しく知りたい方は、次の記事を読むとよいでしょう。

notchained.hatenablog.com

sinhrks.hatenablog.com

ただし、実は現在のpurrrのバージョンは0.2で、今後バージョン1になるまでに後方互換性を壊すような変更もありえます。 また、記事で取り上げられているのが古いバージョン(0.1)の場合、注意が必要です。

ただし、map, reduce, keep(filter関数に対応)などの機能は、 Python等の他の言語にも見られる一般的なものなので、そこまで大きな変更はないと個人的には思っています。

R6によるオブジェクト指向プログラミング

RubyやPythonを使っていると、小さなコードがだんだん大きくなってきたとき、 クラスを作ってロジックを整理したくなると思います。

Rも一応オブジェクト指向言語で、R3クラスやR4クラスなどの機能があるのですが、 他の言語の概念とはかなり異なるもので、正直私も理解できていません…。 他言語のプログラマーの想像する「普通の」オブジェクト指向を使うには、R6というパッケージを使いましょう。

qiita.com

R6ライブラリを使って、ChatworkのAPIを叩くクラスを作ってみた事があったので、そちらを例に出してみます。 R6::R6Classの関数の第一引数にクラス名、第二引数にpublicの変数と関数のリスト、第三引数にprivateの変数と関数のリストを渡します。 ここでは触れてませんが、継承もできるそうです。 (継承が必要なほど大きいプログラムを書くなら、他の言語に移植したほうがいい気もしますが…)

library(R6)
library(utils)
library(RCurl)
library(rjson)

api <- R6::R6Class("api",
  public = list(
    initialize = function(access_token) {
      private$access_token <- access_token
    },

    # room_idの部屋にメッセージを投稿する
    post_messages = function(message, room_id) {
      result <- RCurl::postForm(
        uri = private$make_uri('rooms', room_id, 'messages'),
        .opts = list(
          httpheader = private$make_header(),
          postfields = private$make_body(message)
        )
      )
      return(
        rjson::fromJSON(result)
      )
    },
  
  private = list(
    access_token = NA,
    url = 'https://api.chatwork.com/v1',
    
    make_header = function() {
      # ヘッダーを作成する
    },

    # ChatworkのAPIの種類によってURIの階層の長さが違うので、可変長引数で文字列を渡して"/"区切りに変換する関数を用意
    make_uri = function(...) { 
      # URIを作成する
    },
    
    make_body = function(message) {
      # BODYを作成する
    }
  )
)

# インスタンスを作る
client <- api$new(access_token = '82*************')

# Chatworkにメッセージを投稿する
client$post_messages(message = 'Hello Chatwork!', room_id = 41******)
# $message_id
# [1] 995512653

関数の引数で無理やりクラスを作る感じが、JavaScriptのライブラリっぽい気がします。 元々クラスベースのオブジェクト指向じゃない言語が、無理やりクラスを実現するとこんな感じになるんだと思います。

(Rのオブジェクト指向も含めて)詳しい話はこちらの発表資料を見ると良いです。

testthatによる単体テスト

同期のRプログラマーに、「RubyのRSpecみたいなものないの?」と質問したところ、testthatというライブラリを教えてもらいました。

github.com

パッケージの中に開発時にはよく利用されているようです。 GitHubで公開されているRのライブラリのコードを読むと、testthatのテストコードをよく見かけます。

Hacking is believing: R でいまどきなパッケージ開発 (devtools, testthat, roxygen2)

私はきちんと使ったことがないのでこれ以上の紹介はできません。 R6と組み合わせ、クラスの中のprivate関数をテストできるかなど気になるのですが…。

shinyなどを使ったレポートの共有

簡単なデータの可視化ツールや、社内ツールくらいなら簡単に作れます。 サーバー上にRをインストールし、shinyというライブラリを使えば、Rの解析結果をブラウザで表示・URLで共有できるようになります。

tech-sketch.jp

分析のプロセスでいちいち手作業したくない、分析結果をそのまま共有したいという、怠惰なRプログラマーらしいライブラリだと思います。

最近はleafletという地図のライブラリと組み合わせるのが流行っているようですね。

RPubs - leafletではじめるRによる地図プロット