StatsFragments

Python, R, Rust, 統計, 機械学習とか

Rの data.table と data.frame を dplyr で区別なく扱う

R を使っていると、組み込み型の data.frame と大規模データ用パッケージである data.table の差異で思わずはまることがあるので使い方をまとめる。どちらか一方しか使わないようにすれば 差異を気にする必要はないのかも知れないが、、。

基本的には

データ操作用パッケージ dplyrdata.framedata.table 両方に対して同じように使えるので、できるだけ dplyr を使って操作するのがよい。

ある程度 複雑な操作であれば最初から dplyr を使うと思うが、列選択, 行選択, 代入など 比較的シンプルな操作はつい 通常の書式で書いてしまう (そしてはまる、、)。また、列名を文字列に入れて処理するなど、dplyr 0.2以前では(シンプルには)書けない処理もあった。

dplyr 0.3でこのあたりの処理が素直に書けるようになっているので、その方法と 通常の記法で書いた場合にひっかかるポイントをまとめてみた。

まずdplyrのwrapperに渡す

何か処理を始める前には、data.frame, data.tabledplyr で定義された wrapperオブジェクトに渡すこと。それぞれ、dplyr::tbl_df, dplyr::tbl_dt を通しておけば、表示フォーマットなんかをあわせてくれる。特に tbl_dfdata.frame の表示を既定で省略/列選択時の挙動を一貫したものにしてくれる(下記 補足参照)ので必須。

※代入式を括弧で囲んでいるのは 代入 + printするため。通常は必要ない。

(df <- dplyr::tbl_df(iris))
# Source: local data frame [150 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width
# 1           5.1         3.5          1.4         0.2
# 2           4.9         3.0          1.4         0.2
# 3           4.7         3.2          1.3         0.2
# ..          ...         ...          ...         ...

(dt <- dplyr::tbl_dt(data.table::data.table(iris)))
# Source: local data table [150 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width
# 1           5.1         3.5          1.4         0.2
# 2           4.9         3.0          1.4         0.2
# 3           4.7         3.2          1.3         0.2
# ..          ...         ...          ...         ...

以降のサンプルで、 dfdata.frame, dtdata.table をあらわす。また、返ってくるデータ形式に特に差異がない場合の出力は適宜 省略する。

補足: data.frame では 列選択時に結果が 一列になった場合は vector, 複数列の場合は data.frame を返すという謎の挙動をする。tbl_dfはこれを抑制し、常に data.frame を返してくれる (v0.3.0以降)。

# 素の data.frame
# 結果が複数列の場合は data.frame
iris[, c(FALSE, FALSE, FALSE, TRUE, TRUE)]
#     Petal.Width    Species
# 1           0.2     setosa
# 2           0.2     setosa
# 3           0.2     setosa

# 結果が一列の場合に vectorになる
iris[, c(FALSE, FALSE, FALSE, FALSE, TRUE)]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

# tbl_df なら 結果が一列でも data.frame
df[, c(FALSE, FALSE, FALSE, FALSE, TRUE)]
# Source: local data frame [150 x 1]
# 
#    Species
# 1   setosa
# 2   setosa
# 3   setosa
# ..     ...

# data.tableは一貫して data.table
data.table(iris)[, c(FALSE, FALSE, FALSE, FALSE, TRUE), with = FALSE]
# 略 (data.table)

dt[, c(FALSE, FALSE, FALSE, FALSE, TRUE), with = FALSE]
# 略 (data.table)

補足2 一方、data.table[, ]記法で 列名を変数名で指定した場合は vector、文字列で指定した場合は data.tableと挙動が違うので注意。

dt[, Species]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

dt[, 'Species', with = FALSE]
# Source: local data table [150 x 1]
# 
#    Species
# 1   setosa
# 2   setosa
# 3   setosa
# .   ...

なんにせよ、一列を選択する処理では常に戻り値の型を意識しないと つい以下のような処理を書いてしまう。

# OK
is.factor(dt[, Species])
# [1] TRUE

# NG!
is.factor(dt[, 'Species', with = FALSE])
# [1] FALSE

列操作

列名操作

参照は names, colnamesが共通の動作をするのでどちらでもよい。

names(df)
# [1] "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width"  "Species"     

names(dt)
# 略

colnames(df)
# 略
     
colnames(dt)
# 略

列名変更は dplyr::rename (v0.3.0以降)。

dplyr::rename(df, newcol = Species)
# Source: local data frame [150 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width newcol
# 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
# ..          ...         ...          ...         ...    ...

dplyr::rename(dt, newcol = Species)
# 略

列名を文字列で渡したい場合は dplyr::rename_rename_renameStandard evaluation 版。(select_, group_by_など、様々な dplyr関数にv0.3.0で追加された)で、渡した文字列を関数/列名として評価するもの。

dplyr::rename_(df, 'newcol' = 'Species')
# 略

dplyr::rename_(dt, 'newcol' = 'Species')
# 略

すべての列名をまとめて変更したい場合は上の記法で個々に指定してもよいが、 dplyr::rename_(.dots)を使うと便利。dotsに渡したリストは引数として展開された後、Standard Evaluationされる。この記法で可変長の文字列引数を dplyr へ渡せる ( Python でいうと **kwargs 記法のような感じ )。

# .dotsに引数として渡すリスト
setNames(names(df), c('col1', 'col2', 'col3', 'col4', 'col5'))
#           col1           col2           col3           col4           col5 
# "Sepal.Length"  "Sepal.Width" "Petal.Length"  "Petal.Width"      "Species" 

dplyr::rename_(df, .dots = setNames(names(df), c('col1', 'col2', 'col3', 'col4', 'col5')))
# Source: local data frame [150 x 5]
# 
#    col1 col2 col3 col4   col5
# 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
# ..  ...  ...  ...  ...    ...

dplyr::rename_(dt, .dots = setNames(names(dt), c('col1', 'col2', 'col3', 'col4', 'col5')))
# 略

補足 dplyr::renamedata.tableに対して裏側で高速化された data.table::setnames を使ってくれている。setnamesは破壊的だが renameは非破壊的。

names(dt)
# "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width"  "Species"     

names(dplyr::rename(dt, newcol = Species))
# [1] "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width"  "newcol"      

# rename適用しても元データは変更されない
names(dt)
# [1] "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width"  "Species"     

# setnamesでは変更される
data.table::setnames(dt, c('col1', 'col2', 'col3', 'col4', 'col5'))
names(dt)
# [1] "col1" "col2" "col3" "col4" "col5"

※ここで列名変更した場合、以降のスクリプトの前に列名を元に戻すこと。

変数名を用いて、列をベクトルとして選択する

dplyr::select もしくは $ を使う。selectは戻り値が data.frameもしくは data.table になるので、結果を vector としてほしい場合は列番号指定が必要なことに注意。

dplyr::select(df, Species)
# Source: local data frame [150 x 1]
# 
#    Species
# 1   setosa
# 2   setosa
# 3   setosa
# ..     ...

dplyr::select(df, Species)[[1]]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

df$Species
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

dt$Species
# 略

補足 data.tableでは[ , ]記法の際に変数名で列選択できるが、data.frame では使えない。

df[, Species]
# Error in `[.tbl_df`(df, , Species) : object 'Species' not found

dt[, Species]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

補足2 data.frame[, ] で変数名から列選択したいときは substitute %>% deparse すればできる。ただし戻り値は data.frame になるので、vector がほしければ列指定する。

df[, deparse(substitute(Species))][[1]]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica
変数名を用いて、列を data.frame/data.table として選択する

dplyr::select を使う。

dplyr::select(df, Petal.Length, Petal.Width)
# Source: local data frame [150 x 2]
# 
#    Petal.Length Petal.Width
# 1           1.4         0.2
# 2           1.4         0.2
# 3           1.3         0.2
# ..          ...         ...

dplyr::select(dt, Petal.Length, Petal.Width)
# 略
文字列を用いて、列をベクトルとして選択する

dplyr::select_ もしくは [[

dplyr::select_(df, 'Species')[[1]]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

df[['Species']]
# 略

dt[['Species']]
# 略

また、多少長くなってもよければ通常のselectも使える (次セクション参照)。

dplyr::select(df, one_of(c('Species')))[[1]]
# 略
文字列vectorを用いて、列をdata.frame/data.tableとして選択する

dplyr::select + one_ofone_ofに渡しているものはデータと関係ない文字列vectorなので、select_ではなくselectでよい。

※以下の例の場合は stats_withでもOK.

dplyr::select(df, one_of(c('Petal.Length', 'Petal.Width')))
# Source: local data frame [150 x 2]
# 
#    Petal.Length Petal.Width
# 1           1.4         0.2
# 2           1.4         0.2
# 3           1.3         0.2
# ..          ...         ...

dplyr::select(dt, one_of(c('Petal.Length', 'Petal.Width')))
# 略

dplyr::select(df, starts_with('Petal'))
# 略

また、dplyr::select_(.dots)を使って以下のようにも書ける。

dplyr::select_(df, .dots = c('Petal.Width', 'Petal.Length'))
# 略

dplyr::select_(dt, .dots = c('Petal.Width', 'Petal.Length'))
# 略

補足 data.framedata.table それぞれの書式で書く場合は、data.tablewith = FALSEを指定しないと そのまま文字列vector として評価されてしまう。

# df[, c('Petal.Width', 'Species')]
# Source: local data frame [150 x 2]
# 
#    Petal.Width Species
# 1          0.2  setosa
# 2          0.2  setosa
# 3          0.2  setosa
# ..         ...     ...

# NG!
dt[, c('Petal.Length', 'Petal.Width')]
# [1] "Petal.Length" "Petal.Width" 

dt[, c('Petal.Length', 'Petal.Width'), with = FALSE]
# Source: local data table [150 x 2]
# 
#    Petal.Width Species
# 1          0.2  setosa
# 2          0.2  setosa
# 3          0.2  setosa
# ..         ...     ...
真偽値vectorを用いて、列をdata.frame/data.tableとして選択する

dplyr::select + one_of。ただし真偽値 vector はそのままでは selectに渡せないため、names をかませて列名を取る。

dplyr::select(df, one_of(names(df)[c(FALSE, FALSE, TRUE, TRUE, FALSE)]))
# Source: local data frame [150 x 2]
# 
#    Petal.Length Petal.Width
# 1           1.4         0.2
# 2           1.4         0.2
# 3           1.3         0.2
# ..          ...         ...

dplyr::select(dt, one_of(names(dt)[c(FALSE, FALSE, TRUE, TRUE, FALSE)]))
# 略

補足 通常の記法を使う際の with = FALSEは文字列vectorの場合と同様。

df[, c(FALSE, FALSE, TRUE, TRUE, FALSE)]
# 略

dt[, c(FALSE, FALSE, TRUE, TRUE, FALSE), with = FALSE]
# 略
列の属性/値が特定の条件に該当する列を、data.frame/data.tableとして選択する

型が数値の列のみ取り出したい、なんてときには dplyr::select + one_of + sapply

dplyr::select(df, one_of(names(df)[sapply(df, is.numeric)]))
# Source: local data frame [150 x 4]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width
# 1           5.1         3.5          1.4         0.2
# 2           4.9         3.0          1.4         0.2
# 3           4.7         3.2          1.3         0.2
# ..          ...         ...          ...         ...

dplyr::select(dt, one_of(names(dt)[sapply(dt, is.numeric)]))
# 略

補足 通常の記法を使う際h(略)

df[, sapply(df, is.numeric)]
# 略

dt[, sapply(df, is.numeric), with = FALSE]
# 略

行操作

値が特定の条件を満たす行を抽出する

dplyr::filter

dplyr::filter(df, Species == 'virginica')
# Source: local data frame [50 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
# 1           6.3         3.3          6.0         2.5 virginica
# 2           5.8         2.7          5.1         1.9 virginica
# 3           7.1         3.0          5.9         2.1 virginica
# ..          ...         ...          ...         ...     ...

dplyr::filter(dt, Species == 'virginica')
# 略

列名、式を文字列として渡す場合は dplyr::filter_

dplyr::filter_(df, "Species == 'virginica'")
# 略

dplyr::filter_(dt, "Species == 'virginica'")
# 略

補足 通常の記法では data.frameで条件式後のカンマを忘れると悲惨なことになる。列選択との場合と違って、ここのカンマは忘れやすいと思う、、自分だけ? data.table ではカンマ有無どちらでも同じ挙動。

df[df$Species == 'virginica', ]
# Source: local data frame [50 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
# 1           6.3         3.3          6.0         2.5 virginica
# 2           5.8         2.7          5.1         1.9 virginica
# 3           7.1         3.0          5.9         2.1 virginica
# ..          ...         ...          ...         ... ...

# NG!
df[df$Species == 'virginica']
# Source: local data frame [150 x 50]

dt[dt$Species == 'virginica', ]
# Source: local data table [50 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
# 1           6.3         3.3          6.0         2.5 virginica
# 2           5.8         2.7          5.1         1.9 virginica
# 3           7.1         3.0          5.9         2.1 virginica
# ..          ...         ...          ...         ... ...

dt[dt$Species == 'virginica']
# Source: local data table [50 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
# 1           6.3         3.3          6.0         2.5 virginica
# 2           5.8         2.7          5.1         1.9 virginica
# 3           7.1         3.0          5.9         2.1 virginica
# ..          ...         ...          ...         ... ...
特定の行番号を抽出する

n行目を抽出する処理には dplyr::slice

# dplyr::slice(df, 2:4)
# Source: local data frame [3 x 5]
# 
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1          4.9         3.0          1.4         0.2  setosa
# 2          4.7         3.2          1.3         0.2  setosa
# 3          4.6         3.1          1.5         0.2  setosa

dplyr::slice(dt, 2:4)
# 略

ランダムサンプリングしたい場合は、それ用の関数 dplyr::sample_n で直接抽出できる。標準関数でやる場合のようにsampleでインデックスをランダム生成してスライシングする必要はない。

dplyr::sample_n(df, 5)
# Source: local data frame [5 x 5]
# 
#   Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
# 1          4.8         3.4          1.6         0.2     setosa
# 2          5.0         2.3          3.3         1.0 versicolor
# 3          4.9         2.4          3.3         1.0 versicolor
# 4          4.8         3.4          1.9         0.2     setosa
# 5          6.2         3.4          5.4         2.3  virginica

dplyr::sample_n(dt, 5)
# Source: local data table [5 x 5]
# 
#   Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
# 1          5.9         3.2          4.8         1.8 versicolor
# 2          6.1         3.0          4.6         1.4 versicolor
# 3          6.9         3.2          5.7         2.3  virginica
# 4          5.5         2.3          4.0         1.3 versicolor
# 5          4.8         3.4          1.6         0.2     setosa

代入

dplyr::mutate。計算結果を新しい列 "Petal.Mult" として追加するとき、

dplyr::mutate(df, Petal.Mult = Petal.Width * Petal.Length)
# Source: local data frame [150 x 6]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width Species Petal.Mult
# 1           5.1         3.5          1.4         0.2  setosa       0.28
# 2           4.9         3.0          1.4         0.2  setosa       0.28
# 3           4.7         3.2          1.3         0.2  setosa       0.26
# ..          ...         ...          ...         ...     ...        ...

dplyr::mutate(dt, Petal.Mult = Petal.Width * Petal.Length)
# 略

列の値、式が文字列の場合は dplyr::mutate_

dplyr::mutate_(df, 'Petal.Mult' = 'Petal.Width * Petal.Length')
# 略

dplyr::mutate_(dt, 'Petal.Mult' = 'Petal.Width * Petal.Length')
# 略

補足 data.tableの代入式はかっこいいが、列名、式を文字列で扱う場合の挙動が恐ろしすぎるので自分はあまり使っていない。

x = 'Petal.Mult'
y = 'Petal.Witdh * Petal.Length'

# NG!
(data.table(iris)[, x := y])
#      Sepal.Length Sepal.Width Petal.Length Petal.Width   Species                          x
#   1:          5.1         3.5          1.4         0.2    setosa Petal.Witdh * Petal.Length
#   2:          4.9         3.0          1.4         0.2    setosa Petal.Witdh * Petal.Length
#   3:          4.7         3.2          1.3         0.2    setosa Petal.Witdh * Petal.Length
#   ..          ...         ...          ...         ...    ...    ...

# これまでのように with = FALSE しても NG!
(data.table(iris)[, x := y, with = FALSE])
#      Sepal.Length Sepal.Width Petal.Length Petal.Width   Species                 Petal.Mult
#   1:          5.1         3.5          1.4         0.2    setosa Petal.Witdh * Petal.Length
#   2:          4.9         3.0          1.4         0.2    setosa Petal.Witdh * Petal.Length
#   3:          4.7         3.2          1.3         0.2    setosa Petal.Witdh * Petal.Length
#   ..          ...         ...          ...         ...    ...    ...

# OK
(data.table(iris)[, x := get('Petal.Width') * get('Petal.Length'), with = FALSE])
#      Sepal.Length Sepal.Width Petal.Length Petal.Width   Species Petal.Mult
#   1:          5.1         3.5          1.4         0.2    setosa       0.28
#   2:          4.9         3.0          1.4         0.2    setosa       0.28
#   3:          4.7         3.2          1.3         0.2    setosa       0.26
#   ..          ...         ...          ...         ...    ...          ...

まとめ

上述のような基本操作もdplyrによって data.frame, data.table共通の処理に置き換えられるようになった。特に Standard Evaluation関数(アンダースコア付の関数)によって変数名を文字列として扱うことで、かなり柔軟な処理が書けるようになっている。ということで dplyr 使っておけばいい。