大規模データの高速処理 ーdata.table、dplyrー

Rは便利な統計解析ツールですが、処理の遅さや大規模データの扱いにくさが弱点と言われています。 このような状況に対処すべく、現在ではパフォーマンスの向上に役立つパッケージが数多く開発されています。 そこで今回は「Rとウェブの融合」をお休みして、data.tabledplyrによる大規模データの高速処理について紹介します。 この記事では2014年7月現在の最新バージョン(data.table 1.9.2及びdplyr 0.2)を利用しています。 必要に応じてインストールして下さい。また紙面の都合で実行結果の掲載は省略しているので、手元の環境で試して実行結果を確認してみることをお勧めします。

> # パッケージのインストールと読み込み
> install.packages(c("data.table", "dplyr"))
> library(data.table)
> library(dplyr)

data.tableパッケージ

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つ目の例はy1の行とそれ以外の行に分けて平均を計算しています。

> 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パッケージは高速なデータ処理を実現するための便利な関数群を提供しています。 ここで紹介する関数以外にもdplyrパッケージでは窓処理関数やデータベース操作用関数などが提供されています。詳細はvignette(package = "dplyr")としてドキュメントを参照して下さい。

dplyrパッケージではdata.framedata.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の値が46以外の列を抽出しています。

> 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()は既存の列の値を処理して新しい列を作成します。 次の例ではv1v2という新しい変数(列)を作成しています。 この例の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()によるグループ化と上述のデータ処理関数を組み合わせることで、このようなデータ集約を簡単に行うことができます。次の例ではamvsの各水準の行数(n())、平均、標準偏差を求めています。

> mtcars2 <- group_by(mtcars, am, vs)
> summarise(mtcars2, cnt = n(), mpg.m = mean(mpg), mpg.sd = sd(mpg))

チェイン構文

特定の行を抽出してから集約を行うなど、 データ処理関数を続けて実行する状況では%>%によるチェイン構文を利用すると便利です。 次の例ではmtcarsmpg15以上の行を抽出してamvsによりグループ化した上で、 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.tabledplyrは、慣れれば慣れるほどその便利さを実感できるようになります。 最初は戸惑うこともあるかもしれませんが、ぜひ導入してみてください。 コードの記述も、データ処理にかかる時間も、大幅に削減できること間違いありません。

次回は再び「Rとウェブの融合」に戻り、rmarkdownパッケージによるレポート作成術について紹介します。