Rの data.table と data.frame を dplyr で区別なく扱う
R を使っていると、組み込み型の data.frame
と大規模データ用パッケージである data.table の差異で思わずはまることがあるので使い方をまとめる。どちらか一方しか使わないようにすれば 差異を気にする必要はないのかも知れないが、、。
基本的には
データ操作用パッケージ dplyr が data.frame
と data.table
両方に対して同じように使えるので、できるだけ dplyr
を使って操作するのがよい。
ある程度 複雑な操作であれば最初から dplyr
を使うと思うが、列選択, 行選択, 代入など 比較的シンプルな操作はつい 通常の書式で書いてしまう (そしてはまる、、)。また、列名を文字列に入れて処理するなど、dplyr
0.2以前では(シンプルには)書けない処理もあった。
dplyr
0.3でこのあたりの処理が素直に書けるようになっているので、その方法と 通常の記法で書いた場合にひっかかるポイントをまとめてみた。
まずdplyrのwrapperに渡す
何か処理を始める前には、data.frame
, data.table
を dplyr
で定義された wrapperオブジェクトに渡すこと。それぞれ、dplyr::tbl_df
, dplyr::tbl_dt
を通しておけば、表示フォーマットなんかをあわせてくれる。特に tbl_df
は data.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 # .. ... ... ... ...
以降のサンプルで、 df
は data.frame
, dt
は data.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_
は rename
の Standard 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::rename
は data.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_of
。one_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.frame
と data.table
それぞれの書式で書く場合は、data.table
で with = 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
使っておけばいい。