Rは便利な統計解析ツールですが、処理の遅さや大規模データの扱いにくさが弱点と言われています。
このような状況に対処すべく、現在ではパフォーマンスの向上に役立つパッケージが数多く開発されています。
そこで今回は「Rとウェブの融合」をお休みして、data.table
とdplyr
による大規模データの高速処理について紹介します。
この記事では2014年7月現在の最新バージョン(data.table 1.9.2及びdplyr 0.2)を利用しています。
必要に応じてインストールして下さい。また紙面の都合で実行結果の掲載は省略しているので、手元の環境で試して実行結果を確認してみることをお勧めします。
> # パッケージのインストールと読み込み
> install.packages(c("data.table", "dplyr"))
> library(data.table)
> library(dplyr)
data.table
パッケージは大規模データを高速に処理できるようにR組み込みのdata.frame
クラスを拡張したものです。
data.frame()
関数と同じようにdata.table()
関数によりdata.table
オブジェクトを作成できます。
> dt <- data.table(x=rep(c("a","b","c"),each=3), y=c(1,3,6), v=1:9)
> dt
x y v
1: a 1 1
2: a 3 2
# 略
8: c 3 8
9: c 6 9
また、data.frame
オブジェクトをdata.table
オブジェクトに変換することもできます。
なお、data.table
には行名という概念がありません。
行名を保存したい場合は引数keep.rownames=TRUE
とすればrn
という列に行名が保存されます。
> data.table(mtcars)
> data.table(mtcars, keep.rownames=TRUE) # 行名を保存
fread()
関数によってディスクから大規模データを高速に読み込むことができます。
read.table()
などよりも自由度は落ちますが、処理速度は遥かに高速です。
以下の例では50MBのCSVファイルのロードに要する時間を表示しています。30倍近く高速化されています。
> system.time(x <- read.csv("test.csv"))
ユーザ システム 経過
14.623 0.170 14.798
> system.time(x <- fread("test.csv"))
ユーザ システム 経過
0.519 0.021 0.539
data.table
オブジェクトに対してsetkey()
またはsetkeyv()
関数によりキー列を設定できます。
キーを設定すると、そのdata.table
オブジェクトはキー列でソートされます。
キーを設定しなくても検索や集約は可能ですが、予めキーを設定することでキー列に対する高速な検索やキー列を利用した高速なデータ集約などが可能となります。
> setkey(dt, x) # キー列の設定
> setkeyv(dt, "x") # 文字列によるキー列の指定
data.table
オブジェクトは添字表記[]
を通して要素の抽出や変更、グループ化や集約などの操作が可能です。
多少不正確ですが大雑把に言うと、[]
の第1引数で行に関する操作、第2引数で列に関する操作を行います。
> dt[2:4] # インデクスによる行の抽出
> dt[y == 3] # 条件式による行の検索(低速)
> dt["a"] # キー列による行の検索(高速)
> dt[, list(x, y)] # 列名による列の選択
> dt[, 1:2, with = FALSE] # インデクスによる列の選択
> dt[, c("x", "y"), with = FALSE] # 列名(文字列)による選択
行と列の抽出を同時に行うこともできます。
> dt[y == 3, list(x, y)]
内容を変更するには添字表記[]
の2番目の引数で:=
構文を使います。
なお、data.table
では通常のRオブジェクトとは異なり、処理対象とするオブジェクトの内容が更新されるので、結果を変数に代入する必要はありません。
> dt[, z:=v*2] # 列を追加
> dt[, z:=NULL] # 列を削除
> dt[, v:=-v] # 要素を変更
> dt["a", y:=-y] # 特定の列の要素を変更
添字表記[]
の第3引数にby
に変数名や条件式を渡すことデータのグループ化と集約を行うことができます。
次の1つ目の例ではx
の水準ごとの平均を計算しています。2つ目の例はy
が1
の行とそれ以外の行に分けて平均を計算しています。
> dt[, mean(v), by = x] # 水準による集約
> dt[, mean(v), by = y == 1] # 条件による集約
> f <- function(a, b) a + b
> dt[, list(x, z = f(y, v))] # 自作関数による集約
data.table
のオブジェクトを関数などに渡した時、関数内で変更が行われると元のオブジェクトも更新されます。
これを防ぐにはcopy()
により明示的にコピーを作成して関数に渡します。
> fun(copy(dt))
data.table
パッケージについて更に詳細な利用方法については『R言語上級ハンドブック』(C&R研究所) セクション34も参考にして下さい。
dplyr
パッケージは高速なデータ処理を実現するための便利な関数群を提供しています。
ここで紹介する関数以外にもdplyr
パッケージでは窓処理関数やデータベース操作用関数などが提供されています。詳細はvignette(package = "dplyr")
としてドキュメントを参照して下さい。
dplyr
パッケージではdata.frame
やdata.table
オブジェクトを処理することができます。
巨大なデータを扱うとRコンソール上での表示が煩わしい場合があります。
このような場合には、
tbl_df()
関数でdata.frame
オブジェクトをtbl_df
オブジェクトに、
tbl_dt()
関数でdata.table
オブジェクトをtbl_dt
オブジェクトに変換することで、データの表示が簡潔になります。
また、glimpse()
を用いるとリスト形式でデータを表示することができます。
> df <- tbl_df(iris)
> df
> dt <- tbl_dt(data.table(iris))
> dt
> glimpse(iris)
dplyr
パッケージでは5種類のデータ処理関数を組み合わせてデータの検索、抽出、集約などを行います。
これらの関数では、第1引数にデータ、それ以降の引数にパラメータを渡します。
関数名 | 機能 |
---|---|
filter |
条件を満たす行の抽出 |
arrange |
行の並べ替え |
select |
列の抽出 |
mutate |
新しい列の作成 |
summarise |
データの集計 |
filter()
では複数の条件式を渡すことができます。
次の例では変数am
の値が1
で、かつ変数cyl
の値が4
と6
以外の列を抽出しています。
> filter(mtcars, am == 1, !cyl %in% c(4, 6))
arrange()
に複数の列名を渡すことでネストしたソートを行うことができます。
またdesc()
と使うことで逆順ソートが可能です。
次の例ではam
で順ソートした上で、cyl
で逆順ソートしています。
> arrange(mtcars, am, desc(cyl))
select()
では、列名を指定する他、列名に対するパターン検索を利用して抽出することもできます。
> select(mtcars, am, vs)
> select(iris, contains("Width")) # 列名に"Width"を含む列を選択
mutate()
は既存の列の値を処理して新しい列を作成します。
次の例ではv1
とv2
という新しい変数(列)を作成しています。
この例のv2
のように引数の中で作成した変数(v1
)を利用することも可能です。
> mutate(mtcars, v1 = hp/cyl, v2 = v1/wt)
summarise()
は複数の行の値を集計します。
次の例では変数mpg
の平均と標準偏差を計算しています。
summarise()
と次節で述べるグループ化と組み合わせることで、データの集約を非常に簡単に行えるようになります。
> summarise(mtcars, m = mean(mpg), sd = sd(mpg))
ここでは基本的な使い方のみ取り上げました。詳細は?manip
としてヘルプファイルを参照して下さい。
実用的なデータ処理では、ある変数の水準別に集計を行う場面が頻出します。
dplyr
ではgroup_by()
によるグループ化と上述のデータ処理関数を組み合わせることで、このようなデータ集約を簡単に行うことができます。次の例ではam
、vs
の各水準の行数(n()
)、平均、標準偏差を求めています。
> mtcars2 <- group_by(mtcars, am, vs)
> summarise(mtcars2, cnt = n(), mpg.m = mean(mpg), mpg.sd = sd(mpg))
特定の行を抽出してから集約を行うなど、
データ処理関数を続けて実行する状況では%>%
によるチェイン構文を利用すると便利です。
次の例ではmtcars
のmpg
が15
以上の行を抽出してam
とvs
によりグループ化した上で、
v1
という新しい列を作成して、そのグループごとの平均を求めています。
> mtcars %>% filter(mpg > 15) %>%
+ group_by(am, vs) %>%
+ mutate(v1 = hp/cyl) %>%
+ summarise(v1.m = mean(v1))
チェイン構文を使わずに記述すると次のようになります。 好みの構文を使いましょう(著者はチェイン構文をオススメします)。
> summarize(
+ mutate(
+ group_by(
+ filter(mtcars, mpg > 15),
+ am, vs),
+ v1 = hp/cyl),
+ v1.m = mean(v1))
チェイン構文ではdplyr
パッケージ以外の関数も利用できます。
この場合、関数の最初の引数にデータが渡されます。
次の例では簡単な例としてチェイン構文によってsummary()
を適用しています。
> mtcars %>% summary
do()
関数を使うと、複雑な結果を返す関数をdplyr
のデータ処理関数と組み合わせて使うことができます。
次の例ではグループ別に回帰を行っています。関数内では.
(ピリオド)によりデータを参照します。
なお、処理の結果はリストオブジェクトがデータフレームに押し込まれた形になります。また変数名(次の例の場合fit
)を明示する必要があります。
> mtcars %>% group_by(am, vs) %>%
+ do(fit = lm(mpg ~ disp, data = .))
複数の列に同じ処理を行いたい場合にはsummarise_each()
やmutate_each()
関数が便利です。
第1引数でデータ、第2引数で適用する関数(funs()
で関数リストを作成するか、関数名を文字列で渡します)、それ以降の引数で処理対象とする列(select()
と同じ形式が使えます)を指定します。
次の1つ目の例ではSpecies
以外の全ての値を半分にしています。
2つ目の例ではSpecies
でグループ化して各列の平均と標準偏差を求めています。
> iris %>% mutate_each(funs(h = ./2), -Species)
> iris %>% group_by(Species) %>% summarise_each(funs(mean, sd))
今回紹介したdata.table
やdplyr
は、慣れれば慣れるほどその便利さを実感できるようになります。
最初は戸惑うこともあるかもしれませんが、ぜひ導入してみてください。
コードの記述も、データ処理にかかる時間も、大幅に削減できること間違いありません。
次回は再び「Rとウェブの融合」に戻り、rmarkdown
パッケージによるレポート作成術について紹介します。