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 パッケージの %>>% のほうが高速らしい。 https://renkun-ken.github.io/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(.data$Sepal.Length) # pronoun => Sepal.Length
iris %>% dplyr::select(!!Sepal.Length)     # unquote => Petal.Width, Species
iris %>% dplyr::select(!!!rlang::syms(Sepal.Length))  # Petal.Width, Species

これらの指定方法は rename()pull() でも有効。 一方、文字列を受け取れない distinct()group_by() などの関数には普通のunquoteは通用しない。 最後の例のように rlang::syms() でシンボル化して unquote-splicing して渡す必要がある。

columns = c('Petal.Width', 'Species')
iris %>% distinct(!!as.name(columns[1L]))
iris %>% distinct(!!!rlang::syms(columns))

詳しくは宇宙本第3章のコラム 「selectのセマンティクスとmutateのセマンティクス、tidyeval」を参照。

列の値によって選べる亜種 dplyr::select_if(.tbl, .predicate, ...) もある。

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

変数に入った文字列を使う場合もmutate()と同様にunquotingで:

old_name = 'Species'
new_name = toupper(old_name)
iris %>% dplyr::rename(!!new_name := !!old_name) %>% head(2)
  Sepal.Length Sepal.Width Petal.Length Petal.Width SPECIES
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa

名前付きベクターと unquote-splicing を使えば一括指定できる:

named_vec = setNames(names(iris), LETTERS[1:5])
iris %>% dplyr::rename(!!!named_vec) %>% head(2L)
    A   B   C   D      E
1 5.1 3.5 1.4 0.2 setosa
2 4.9 3.0 1.4 0.2 setosa

リネーム関数を渡せる亜種:
rename_all(.tbl, .funs = list(), ...)
rename_at(.tbl, .vars, .funs = list(), ...)
rename_if(.tbl, .predicate, .funs = list(), ...)

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')

複数列で評価する亜種:
filter_all(.tbl, .vars_predicate)
filter_if(.tbl, .predicate, .vars_predicate)
filter_at(.tbl, .vars, .vars_predicate)

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用の代入演算子 := を使う:

y = 'new_column'
x = "Sepal.Length"

# unquoting only right hand side
iris %>% dplyr::mutate(y = log(!!as.name(x))) %>% head(2)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species        y
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 both sides
iris %>% dplyr::mutate(!!y := log(!!as.name(x))) %>% 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

複数列を変更する亜種:
mutate_all(.tbl, .funs, ...)
mutate_at(.tbl, .vars, .funs, ..., .cols = NULL)
mutate_each(.tbl, .funs, ...)
mutate_if(.tbl, .predicate, .funs, ...)

dplyr::transmute(.data, ...)

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

複数列を変更する亜種:
transmute_all(.tbl, .funs, ...)
transmute_at(.tbl, .vars, .funs, ..., .cols = NULL)
transmute_if(.tbl, .predicate, .funs, ...)

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::add_tally() は元の形を維持したままカウント列を追加。

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

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

dplyr::add_count() は元の形を維持したままカウント列を追加。

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

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

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

.data %>% arrange(desc(wt)) %>% head(n) を一撃で、グループごとに。 ボーダーに wt の等しい行がある場合は、すべて保持されてn行以上の出力になる。 デフォルトで降順、すなわち arrange() と逆になることに注意。 昇順で取りたいときは n にマイナス指定か wtdesc(X) (昇順なのに!)。

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::case_when() も便利。
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::row_number(x)
rank(x, ties.method = "first", na.last = "keep") のショートカット。 グループ毎に連番を振るのに便利。
dplyr::ntile(x, n)
数値ベクトル x を順位によって n 個のクラスに均等分け
dplyr::n_distinct(x)
高速で簡潔な length(unique(x))
dplyr::n_groups(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 {} ... のショートカット。

グループ化

tidyr でネストして、 purrr でその list of data.frames に処理を施し、 dplyr でその変更を元の data.frame に適用する、 というのがtidyverse流のモダンなやり方らしい。

iris %>%
  dplyr::group_nest(Species) %>%
  dplyr::mutate(data= purrr::map(data, head)) %>%
  tidyr::unnest()
dplyr::group_by(.data, ..., add=FALSE)
グループごとに区切って次の処理に渡す。 e.g. summarise(), tally() など
dplyr::group_data(.data)

グループ情報を参照:

iris %>% group_by(Species) %>% group_data()
#       Species                       .rows
#        <fctr>                      <list>
# 1:     setosa             1,2,3,4,5,6,...
# 2: versicolor       51,52,53,54,55,56,...
# 3:  virginica 101,102,103,104,105,106,...

左側のキー列だけ欲しければ dplyr::group_keys()
右端の行番号だけ欲しければ dplyr::group_rows()

dplyr::group_nest(.tbl, ..., .key = "data", keep = FALSE)

入れ子 data.frame を作る。 group_by(...) %>% tidyr::nest() のショートカット。

dplyr::group_split(.tbl, ..., keep = FALSE)

list of data.frames に分割する。

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に入らないような型でも大丈夫になる。

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

関連書籍