Heavy Watal

dplyr — 高速data.frame処理

data.frameに対して抽出(select, filter)、部分的変更(mutate)、要約(summarise)、ソート(arrange)などの処理を施すためのパッケージ。 前作 plyr のうちdata.frameに関する部分が強化されている。 purrrtidyr と一緒に使うとよい。

tidyverse に含まれているので、 install.packages('tidyverse') で一括インストール、 library(tidyverse) で一括ロード。

関数の連結 %>%

dplyrではなくmagrittrの機能。

x %>% f(a, b)
これは f(x, a, b) と等価。 左の値 x を第一引数として右の関数 f() に渡す。 一時変数を作ったり、関数を何重にも重ねたりすることなく、 適用する順に次々と処理を記述することができるようになる。 慣れれば書きやすく読みやすい。
library(tidyverse)

## with piping
iris %>%
   dplyr::filter(Species != 'setosa') %>%
   dplyr::select(-dplyr::starts_with('Sepal')) %>%
   dplyr::mutate(petal_area=Petal.Length * Petal.Width * 0.5) %>%
   dplyr::group_by(Species) %>%
   dplyr::summarise_all(funs(mean))

## with a temporary variable
x = iris
x = dplyr::filter(x, Species != 'setosa')
x = dplyr::select(x, -dplyr::starts_with('Sepal'))
x = dplyr::mutate(x, petal_area=Petal.Length * Petal.Width * 0.5)
x = dplyr::group_by(x, Species)
x = dplyr::summarise_all(x, funs(mean))

## with nested functions
dplyr::summarise_all(
  dplyr::group_by(
    dplyr::mutate(
      dplyr::select(
        dplyr::filter(iris, Species != 'setosa'),
        -dplyr::starts_with('Sepal')),
      petal_area=Petal.Length * Petal.Width * 0.5),
    Species),
  funs(mean))

## result
     Species Petal.Length Petal.Width petal_area
1 versicolor        4.260       1.326     2.8602
2  virginica        5.552       2.026     5.6481

現状では magrittr パッケージの %>% が広く採用されているが、 pipeR パッケージの %>>% のほうが高速らしい。 http://renkun.me/pipeR-tutorial/

抽出・絞り込み

dplyr::select(.data, ...)

列を絞る。複数指定、範囲指定、負の指定が可能。 select helper によるパターン指定も便利。 残るのが1列だけでも勝手にvectorにはならずdata.frameのまま。

iris %>% dplyr::select(Petal.Width, Species)
iris %>% dplyr::select('Petal.Width', 'Species')
iris %>% dplyr::select(c('Petal.Width', 'Species'))
iris %>% dplyr::select(4:5)
iris %>% dplyr::select(-c(1:3))
iris %>% dplyr::select(-(Sepal.Length:Petal.Length))
iris %>% dplyr::select(matches('^Petal\\.Width$|^Species$'))

文字列変数で指定しようとすると意図が曖昧になるので、 unquoting やpronounで明確に:

Sepal.Length = c('Petal.Width', 'Species')
iris %>% dplyr::select(Sepal.Length))      # ambiguous!
iris %>% dplyr::select(UQ(Sepal.Length))   # unquote => Petal.Width, Species
iris %>% dplyr::select(!!Sepal.Length)     # unquote => Petal.Width, Species
iris %>% dplyr::select(.data$Sepal.Length) # pronoun => Sepal.Length
dplyr::pull(.data, var=-1)

指定した1列をvector(またはlist)としてdata.frameから抜き出す。

iris %>% head() %>% dplyr::pull(Species)
iris %>% head() %>% dplyr::pull('Species')
iris %>% head() %>% dplyr::pull(5)
iris %>% head() %>% dplyr::pull(-1)
iris %>% head() %>% `[[`('Species')
iris %>% head() %>% {.[['Species']]}
iris %>% head() %>% {.$Species}
{iris %>% head()}$Species

dplyr::filter(.data, ...)

条件を満たす行だけを返す。base::subset() と似たようなもの。

iris %>% dplyr::filter(Sepal.Length<6, Sepal.Width>4)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.7         4.4          1.5         0.4  setosa
2          5.2         4.1          1.5         0.1  setosa
3          5.5         4.2          1.4         0.2  setosa

評価結果が NA となる行は除去される。 特に不等号を使うときやや直感に反するので要注意。 e.g., filter(gene != 'TP53')

dplyr::distinct(.data, ..., .keep_all=FALSE)

指定した列に関してユニークな行のみ返す。 base::unique.data.frame() よりも高速で、 filter(!duplicated(.[, ...])) よりスマートで柔軟。 指定しなかった列を残すには .keep_all=TRUE とする。

iris %>% dplyr::distinct(Species)
     Species
1     setosa
2 versicolor
3  virginica
dplyr::slice(.data, ...)

行番号を指定して行を絞る。 `[`(i,) の代わりに。

iris %>% dplyr::slice(2:4)
  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::sample_n(tbl, size, replace=FALSE, weight=NULL)

指定した行数だけランダムサンプルする。 割合指定の sample_frac() もある。

列の変更・追加

dplyr::mutate(.data, ...)

既存の列を変更したり、新しい列を作ったり。 base::transform() の改良版。

# modify existing column
iris %>% dplyr::mutate(Sepal.Length = log(Sepal.Length))

# create new column
iris %>% dplyr::mutate(ln_sepal_length = log(Sepal.Length))

変数に入った文字列を変更先の列名に指定したい場合は unquoting用の代入演算子 := を使う:

new_column = 'ln_sepal_length'

# normal
iris %>% dplyr::mutate(new_column = log(Sepal.Length)) %>% head(2)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species new_column
1          5.1         3.5          1.4         0.2  setosa   1.629241
2          4.9         3.0          1.4         0.2  setosa   1.589235

# unquoting
iris %>% dplyr::mutate(!!new_column := log(Sepal.Length)) %>% head(2)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species ln_sepal_length
1          5.1         3.5          1.4         0.2  setosa        1.629241
2          4.9         3.0          1.4         0.2  setosa        1.589235
dplyr::transmute(.data, ...)

指定した列以外を保持しない版 mutate() 。 言い換えると、列の中身の変更もできる版 select()

dplyr::rename(.data, ...)

列の改名。 mutate()と同じようなイメージで new=old と指定。

iris %>% dplyr::rename(sp=Species) %>% head(2)
  Sepal.Length Sepal.Width Petal.Length Petal.Width     sp
1          5.1         3.5          1.4         0.2 setosa
2          4.9         3.0          1.4         0.2 setosa

data.frameの要約・集計・整列

dplyr::summarise(.data, ...)

指定した列に関数を適用して1行のdata.frameにまとめる。 グループ化されていたらグループごとに適用して bind_rows() する。

iris %>% dplyr::group_by(Species) %>%
    dplyr::summarise(minpl=min(Petal.Length), maxsw=max(Sepal.Width))
     Species minpl maxsw
1     setosa   1.0   4.4
2 versicolor   3.0   3.4
3  virginica   4.5   3.8
dplyr::summarise_all(.data, .funs, ...)

全てのカラムに関数を適用する。 ***_each() は非推奨になった。

iris %>% dplyr::group_by(Species) %>%
    dplyr::summarise_all(funs(min, max))
dplyr::summarise_at(.data, .cols, .funs, ...)

select補助関数を使って指定したカラムに関数を適用する。

iris %>% dplyr::group_by(Species) %>%
    dplyr::summarise_at(vars(dplyr::ends_with("Width")), funs(min, max))
dplyr::summarise_if(.data, .predicate, .funs, ...)

.predicateがTRUEになるカラムだけに関数を適用する。

iris %>% dplyr::group_by(Species) %>%
    dplyr::summarise_if(is.numeric, funs(min, max))
dplyr::tally(x, wt, sort=FALSE)

summarise(x, n=n()) のショートカット。 wt にカラムを指定して重み付けすることもできる。

dplyr::count(x, ..., wt=NULL, sort=FALSE)

group_by(...) %>% tally() のショートカット。

dplyr::arrange(.data, column1, column2, ...)

指定した列の昇順でdata.frameの行を並べ替える。 arrange(desc(column)) で降順になる。 order() を使うよりもタイピングの繰り返しが少ないし直感的

.data[with(.data, order(col_a, col_b)), ]
# is equivalent to
.data %>% dplyr::arrange(col_a, col_b)
dplyr::top_n(.data, n, wt=NULL)

.data %>% arrange(wt) %>% head(n) を一撃で、グループごとに。

data.frameを結合

dplyr::***_join(x, y, by=NULL, copy=FALSE)

by で指定した列がマッチするように行を合わせて cbind()

full_join(): xy の全ての行を保持。
inner_join(): xyby がマッチする行のみ
left_join(): x の全ての行を保持。y に複数マッチする行があったらすべて保持。
right_join(): y の全ての行を保持。x に複数マッチする行があったらすべて保持。
semi_join(): x の全ての行を保持。y に複数マッチする行があっても元の x の行だけ保持。
anti_join(): y にマッチしない x の行のみ。

列名が異なる場合は by を名前付きベクタにすればよい

dplyr::bind_rows(...), dplyr::bind_cols(...)

標準の rbind(), cbind() より効率よくdata.frameを結合。 引数は個別でもリストでもよい。 そのほかにも標準の集合関数を置き換えるものが提供されている: intersect(), union(), union_all(), setdiff(), setequal()

その他の関数

主にmutate()filter()を補助するもの

dplyr::if_else(condition, true, false, missing=NULL)
標準のifelse()よりも型に厳しく、高速らしい。 NAのときにどうするかを指定できるのも大変良い。
dplyr::coalesce(x, ...)
最初のvectorでNAだったとこは次のvectorのやつを採用、 というifelse(!is.na(x), x, y)的な処理をする。 基本的には同じ長さのvectorを渡すが、 2つめに長さ1のを渡してtidyr::replace_na()的に使うのも便利。
dplyr::na_if(x, y)
x[x == y] = NA; x のショートカット
dplyr::recode(.x, ..., .default=NULL, .missing=NULL)
vectorの値を変更する。e.g., recode(letters, a='A!', c='C!')
dplyr::ntile(x, n)
数値ベクトル x を順位によって n 個のクラスに均等分け
dplyr::n_distinct(x)
高速で簡潔な length(unique(x))
dplyr::last(x, order_by=NULL, default=default_missing(x))
最後の要素にアクセス。 x[length(x)]tail(x, 1) よりも楽チンだが、 安全性重視の dplyr::nth() を内部で呼び出すため遅い。
dplyr::lead(x n=1, default=NA, order_by=NULL), dplyr::lag(...)

x の中身を n だけずらして default で埋める。 lead() は前に、lag() は後ろにずらす

> lag(seq_len(5), 2)
[1] NA NA 1 2 3
dplyr::between(x, left, right)

left <= x & x <= right のショートカット。

dplyr::near(x, y, tol=.Machine$double.eps^0.5)

abs(x - y) < tol のショートカット。

dplyr::case_when(...)

if {} else if {} else if {} ... のショートカット。

グループ化

dplyr だけでグループ化してグループ毎処理するアプローチは今後廃れる見込み。 tidyr でネストして、 purrr でその list of data.frames に処理を施し、 dplyr でその変更を元の data.frame に適用する、 というのがtidyverse流のモダンなやり方らしい。

## OLD
iris %>%
  dplyr::group_by(Species) %>%
  dplyr::do(head(.))

## NEW
iris %>%
  tidyr::nest(-Species) %>%
  dplyr::mutate(data= purrr::map(data, head)) %>%
  tidyr::unnest()
dplyr::group_by(.data, col1, col2, ..., add=FALSE)
グループごとに区切って次の処理に渡す。 e.g. summarise(), tally(), do() など
dplyr::group_indices(.data, ...)
grouped_df ではなくグループIDとして1からの整数を返す版 group_by()
dplyr::rowwise(.data)
行ごとに区切って次の処理に渡す。
dplyr::do(.data, ...)
グループごとに処理する。 {} 内に長い処理を書いてもいいし、関数に渡してもよい。 グループごとに切りだされた部分は . で参照できる。 出力がdata.frameじゃないと Error: Results are not data frames at positions: 1 のように怒られるが、 do(dummy=func(.)) のように名前付きにすると data.frameに入らないような型でも大丈夫になる。

関連書籍