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(), ...)

pattern = c("^Sepal" = "Gaku", "^Petal" = "Kaben")
iris %>% head() %>%
  dplyr::rename_all(stringr::str_replace_all, pattern) %>%
  print()
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() を使うよりもタイピングの繰り返しが少ないし直感的

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

wt で指定した列(省略時は右端の列)の降順で n 行だけ返す。 境界にタイがある場合はすべて保持されて n 行以上の出力になる。 元の順番が保持されるので、ソートされた結果が欲しい場合は事後に arrange() を適用することになる。 順序の方向が 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流のモダンなやり方らしい。 それをもうちょいスマートにやる group_modify() がv0.8.1で導入された。

iris %>%
  dplyr::group_nest(Species) %>%
  dplyr::mutate(data = purrr::map(data, head, n = 2L)) %>%
  tidyr::unnest()

# since dplyr 0.8.1
iris %>%
  dplyr::group_by(Species) %>%
  dplyr::group_modify(~ head(.x, 2L))
dplyr::group_by(.data, ..., add = FALSE, .drop = group_by_drop_default(.data))
グループごとに区切って次の処理に渡す。 e.g. summarise(), tally(), group_modify() など
.drop = FALSE とすると行数ゼロになるグループも捨てずに保持できる。
dplyr::group_data(.data)

グループ情報を参照:

iris %>% dplyr::group_by(Species) %>% dplyr::group_data()
# tbl_df [3 x 2]
#      Species      .rows
#        <fct>     <list>
# 1     setosa <int [50]>
# 2 versicolor <int [50]>
# 3  virginica <int [50]>

左側のキー列だけ欲しければ 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::group_modify(.tbl, .f, ...)

グループごとに .f を適用してまとめたdata.frameを返す。 「それぞれの結果の長さが1」という制約がないぶん summarise() よりも柔軟。

.f にはpurrrでよく見る無名関数を渡す。 (普通の関数も渡せるはずだけどちょっと挙動が変?)

iris %>% dplyr::group_by(Species) %>% dplyr::group_modify(~ head(.x, 2L))
# # grouped_df [6 x 5]
# # Groups: Species [3]
#      Species Sepal.Length Sepal.Width Petal.Length Petal.Width
#        <fct>        <dbl>       <dbl>        <dbl>       <dbl>
# 1     setosa          5.1         3.5          1.4         0.2
# 2     setosa          4.9         3.0          1.4         0.2
# 3 versicolor          7.0         3.2          4.7         1.4
# 4 versicolor          6.4         3.2          4.5         1.5
# 5  virginica          6.3         3.3          6.0         2.5
# 6  virginica          5.8         2.7          5.1         1.9

group_map() は結果を bind_rows() せずlistとして返す亜種。 group_walk().f 適用前の .tbl を返す亜種。

dplyr::do(.data, ...)

非推奨。 代わりに group_modify() とかを使う。

iris %>% dplyr::group_by(Species) %>% dplyr::do(head(., 2L))
dplyr::rowwise(.data)

非推奨。 代わりにpurrr::pmap()とかを使う。

matrix, array

data.frame を主眼とする dplyr では matrix や array を扱わないという方針かと思いきや、意外とそうでもなかった。

as.tbl_cube(x, dim_names, met_names, ...)
matrix/arrayからdata.frameの一歩手前に変換する。 reshape2::melt の改良版。 これの結果に tibble::as_tibble() を適用するとわかりやすい。
ただし dimnames(x) が空ではダメで、長さの正しい名前付きlistになっている必要がある。
# deprecated
iris3 %>%
  reshape2::melt() %>%
  tibble::as_tibble() %>%
  dplyr::rename(obs = Var1, metrics = Var2, species = Var3)

# new
x = iris3
dimnames(x)[[1L]] = seq_len(dim(iris3)[[1L]])
names(dimnames(x)) = c("obs", "metrics", "species")
dplyr::as.tbl_cube(x, met_name = "value") %>%
  tibble::as_tibble()

関連書籍