須通り
Sudo Masaaki official site
For the reinstatement of
population ecology.

おまえが長くtidyverseを覗くならば、tidyverseもまた等しくおまえを見返すのだ。

ホーム | 統計 Top | Tidyverseによるデータフレーム加工(03)dplyr::mutate によるデータ加工とその周辺

本記事の前編 「Tidyverseによるデータフレーム加工(02)dplyr::*_join による複数データフレームの結合/欠損値補完/ルックアップ」 では dplyr の機能のうち、主として複数のデータフレームを結合するための関数群 *_join() ファミリーに触れた。一方、よく知られている dplyr の中核機能といえばおそらく、単一データフレームに基づく一括データ処理である。本稿では filter(), select() といった、データフレーム内の特定条件を満たす行や列を絞り込む関数、そして mutate() や mutate_at() により、行ごとの並列処理としてデータを書き換える(その結果を新規列に追加する)方法について詳述する。

さらに高度なデータ処理を行う関数として group_by() や do() がある。これらを使いこなすには、「階層構造を持つデータフレーム」という高度な概念を理解することが必須であるから、ページの都合で次回以降扱う。

目次

  1. tidyなデータフレームの行や列を条件で絞り込む
    • データフレームの行を条件で絞り込む dplyr::filter 関数
    • データフレームの列を条件で絞り込む dplyr::select 関数
  2. データフレームの列や行に対してデータ順やラベルを弄る
    • データフレームの列を改名する dplyr::rename 関数
    • データフレームの行を並べ換える dplyr::arrange 関数
  3. tidyなデータフレームの行ごとにデータを操作する
    • データフレームの行ごとにデータを操作する dplyr::mutate() 関数
    • データフレーム中の条件指定した列を一括で mutate する dplyr::mutate_at() 関数と dplyr::mutate_if() 関数
    • 処理対象列の処理後の姿だけをデータフレーム中に残しつつ mutate する dplyr::transmute() 関数
  4. dplyr の関数における非標準評価のこと

tibble の構造に深く関わる group_by(), do(), ungroup() は次の記事で扱う。

あと summarise(), sample_n(), sample_frac() あたりも後で扱う。

tidyなデータフレームの行や列を条件で絞り込む

R環境に取り込んだデータが、紆余曲折の末に tidy なデータフレーム(tibble)形式で得られたとする。次なる問題は、特定の条件を満たす行や列だけにアクセスして可視化や統計処理を行いたいときの、絞り込み手順である。

例として、前に用いた架空の農薬試験データセット "tidypest.merged.csv" に御出で願おう。


library(tidyverse)
tidypest.merged <- readr::read_csv("tidypest.merged.csv")
tidypest.merged

# A tibble: 12 x 7
   Season     Site Treat.Before Treat.After Control.Before Control.After Site.name
   <chr>     <int>        <int>       <int>          <int>         <int> <chr>
 1 August        1            6           4              4             1 Saitama
 2 August        2            2           6              5             0 Nagano
 3 August        3            4           1              5             0 Shizuoka
 4 August        4            1           6              3             0 Osaka
 5 August        5            2           5              4             0 Moscow
 6 August        6            2          NA              8             1 Melbourne
 7 September     1            1           1              2             3 Saitama
 8 September     2            5           2              1             3 Nagano
 9 September     3            3           0              4             3 Shizuoka
10 September     4            2           0              1             5 Osaka
11 September     5            0           2              2             7 Moscow
12 September     6            5           1              4             5 Melbourne

データフレームの行を条件で絞り込む dplyr::filter 関数

まあ難しくはない。


# データフレームの行を条件で絞り込む
tidypest.august <- tidypest.merged %>%
    dplyr::filter(Season=="August", Site>=5)

> tidypest.august
# A tibble: 2 x 7
  Season  Site Treat.Before Treat.After Control.Before Control.After Site.name
  <chr>  <int>        <int>       <int>          <int>         <int> <chr>
1 August     5            2           5              4             0 Moscow
2 August     6            2          NA              8             1 Melbourne

# 複数条件の AND での結合は & でつなぐのが正式。
> dplyr::filter(tidypest.merged, Season=="August" & Site>=5)
# A tibble: 2 x 7
  Season  Site Treat.Before Treat.After Control.Before Control.After Site.name
  <chr>  <int>        <int>       <int>          <int>         <int> <chr>
1 August     5            2           5              4             0 Moscow
2 August     6            2          NA              8             1 Melbourne

# 複数条件の OR での結合は | で。
> dplyr::filter(tidypest.merged, Season=="August" | Site>=5)
# A tibble: 8 x 7
  Season     Site Treat.Before Treat.After Control.Before Control.After Site.name
  <chr>     <int>        <int>       <int>          <int>         <int> <chr>
1 August        1            6           4              4             1 Saitama
2 August        2            2           6              5             0 Nagano
3 August        3            4           1              5             0 Shizuoka
4 August        4            1           6              3             0 Osaka
5 August        5            2           5              4             0 Moscow
6 August        6            2          NA              8             1 Melbourne
7 September     5            0           2              2             7 Moscow
8 September     6            5           1              4             5 Melbourne

データフレームの行を条件で絞り込む
filter(.data, ...)

省略不可な引数
.data    絞り込み対象とする tbl 形式のテーブルデータ。正確に言うと filter() やそれに類する関数は S3 クラスの総称関数であり、与えた .data の型たとえば tbl_df() や dtplyr::tbl_dt() や dbplyr::tbl_dbi() などに応じて内部的に対応する関数がある(つまり外部データベースから持ってきたデータも処理対象にできるということ。気になる人は各自勉強)。

...    .data 内の変数において、絞り込み条件を与える論理述語(logical predicates)。複数条件は & で繋いで指定することができる(須藤注:ただし複数条件をコンマ , で繋いでも AND 条件になるし、その方が一般的だと思う。なお OR 条件は | でつなぐ)。とどのつまり .data の行数と同じ長さの真偽値ベクトルになればおk。最終的にTRUEの行が抽出対象となる。

なお ... 以下で指定した引数は、自動的に "" で囲まれてから、データフレームのコンテクスト内で評価される。ただし unquoting や splicing はサポートされている。これらの概念については vignette("programming") を参照。

データフレームの列を条件で絞り込む dplyr::select 関数

filter() 関数の列バージョンがselectである。たまにどっちがどっちか忘れてしまう。


# データフレームの行を条件で絞り込む
print(dplyr::select(tidypest.merged, starts_with("S")), n=5) # 名前で指定
print(dplyr::select(tidypest.merged, 1, 2, -3), n=5) # position で指定。最初の条件がプラス
print(dplyr::select(tidypest.merged, -1, 2, -3), n=5) # position で指定。最初の条件がマイナス
# 列のリネームを伴う操作。左辺に新しい名前、右辺に従来の変数名またはpositionを指定。
print(dplyr::select(tidypest.merged, Season, City=Site.name, Baseline=5), n=5)

# A tibble: 12 x 3
 Season  Site Site.name
 <chr>  <int> <chr>
1 August     1 Saitama
2 August     2 Nagano
3 August     3 Shizuoka
4 August     4 Osaka
5 August     5 Moscow
# ... with 7 more rows

> print(dplyr::select(tidypest.merged, 1, 2, -3), n=5)
# A tibble: 12 x 2
  Season  Site
  <chr>  <int>
1 August     1
2 August     2
3 August     3
4 August     4
5 August     5
# ... with 7 more rows
このときの式評価フローは「まず白紙の状態から1番目のSeasonを拾う」→「2番目のSiteも拾う」→「3番目のTreat.Beforeは拾わない」の順。そもそも拾う予定は無かったのだから、結果的に3番目の条件は無視される。

>  print(dplyr::select(tidypest.merged, -1, 2, -3), n=5)
# A tibble: 12 x 5
  Site Treat.After Control.Before Control.After Site.name
 <int>       <int>          <int>         <int> <chr>
1     1           4              4             1 Saitama
2     2           6              5             0 Nagano
3     3           1              5             0 Shizuoka
4     4           6              3             0 Osaka
5     5           5              4             0 Moscow
# ... with 7 more rows
このときの式評価フローは「1番目のSeasonを落とし、他を残す」→「うち、2番目のSiteは残す」→「うち、3番目のTreat.Beforeは残す」の順。

> print(dplyr::select(tidypest.merged, Season, City=Site.name, Baseline=5), n=5)
# A tibble: 12 x 3
  Season City     Baseline
  <chr>  <chr>       <int>
1 August Saitama         4
2 August Nagano          5
3 August Shizuoka        5
4 August Osaka           3
5 August Moscow          4
# ... with 7 more rows

上の2例目と3例目の違いに注意。select()の絞り込み条件を position の数字で指定するとき、最初の条件が正値か負値かで、他の列を捨てるか否かが変わる。


データフレームの列を条件で絞り込む。第2引数以下で指定されなかった列は削除される。
select(.data, ...)

データフレームの指定した列を改名する。非指定列も全て残される。
rename(.data, ...)

省略不可な引数
.data    絞り込み対象とする tbl 形式のテーブルデータ。filter() のときと同じく総称的である。

...    .data において列の絞り込み条件を与える1つ、ないしはコンマで区切られた複数の expression。変数の名前を、あたかもそれが何列目かを指定する数字(position)であるかのように扱うことができる。この数字が正であれば、その順番に対応する列が残される。数字にマイナスを付けると、対応する順番の列は削除される。

もし最初の expression が負だったら、select() 関数は自動的に全ての変数を残す設定に切り替わってから、続く expression の評価を開始する(須藤注:デフォルトでは、select() 関数は最初にどの変数も残さない状態から評価をスタートし、expression で明示的に指定された変数だけを拾って出力する)。

絞り込み条件を与える expression を named arguments、すなわち左辺に文字列が入るよう = で繋げば、指定した文字列で変数をリネームできる。

なお ... 以下で指定した引数は、自動的に "" で囲まれてから、列の名前が列のポジションを表すように、データフレームのコンテクスト内で評価される。ただし unquoting や splicing はサポートされている。これらの概念については vignette("programming") を参照。

データフレームの列や行に対してデータ順やラベルを弄る

データフレームの列を改名する dplyr::rename 関数

上の select() 関数のヘルプには、 rename() という関数も同時に記載されている。文字通り列の名前をリネームする関数なのだが、select() との違いは、第2引数以下で指定されなかったその他大勢の列を、出力結果において残すか残さないかという挙動だけである。

データフレームの行を並べ換える dplyr::arrange 関数


# データフレームの行を並べ換える
dplyr::arrange(tidypest.merged, Site.name)
dplyr::arrange(tidypest.merged, desc(Control.Before), Site.name)

> dplyr::arrange(tidypest.merged, Site.name)
# A tibble: 12 x 7
   Season     Site Treat.Before Treat.After Control.Before Control.After Site.name
   <chr>     <int>        <int>       <int>          <int>         <int> <chr>
 1 August        6            2          NA              8             1 Melbourne
 2 September     6            5           1              4             5 Melbourne
 3 August        5            2           5              4             0 Moscow
 4 September     5            0           2              2             7 Moscow
 5 August        2            2           6              5             0 Nagano
 6 September     2            5           2              1             3 Nagano
 7 August        4            1           6              3             0 Osaka
 8 September     4            2           0              1             5 Osaka
 9 August        1            6           4              4             1 Saitama
10 September     1            1           1              2             3 Saitama
11 August        3            4           1              5             0 Shizuoka
12 September     3            3           0              4             3 Shizuoka

> dplyr::arrange(tidypest.merged, desc(Control.Before), Site.name)
# A tibble: 12 x 7
   Season     Site Treat.Before Treat.After Control.Before Control.After Site.name
   <chr>     <int>        <int>       <int>          <int>         <int> <chr>
 1 August        6            2          NA              8             1 Melbourne
 2 August        2            2           6              5             0 Nagano
 3 August        3            4           1              5             0 Shizuoka
 4 September     6            5           1              4             5 Melbourne
 5 August        5            2           5              4             0 Moscow
 6 August        1            6           4              4             1 Saitama
 7 September     3            3           0              4             3 Shizuoka
 8 August        4            1           6              3             0 Osaka
 9 September     5            0           2              2             7 Moscow
10 September     1            1           1              2             3 Saitama
11 September     2            5           2              1             3 Nagano
12 September     4            2           0              1             5 Osaka

arrange(.data, ...)
arrange(.data, ..., .by_group=FALSE) # データが grouped_df であったときに対応するメソッド

省略不可な引数
.data    並べ替え対象とする tbl 形式のテーブルデータ。S3 クラスの総称関数であり、与えた .data の型、たとえば tbl_df() や dtplyr::tbl_dt() や dbplyr::tbl_dbi() などに、それぞれ内部的に対応する関数がある。

...    並べ替え条件に用いたい列を、"" で囲わない列名として与える。複数の場合は、コンマで区切って与える。ここに挙げた優先順に、その変数について昇順になるようにデータ行が並べ替えられる(要するに2番目以降の条件は、タイが発生したときの判定基準としてのみ用いられる)。ただし desc() で囲った変数があれば、その変数だけは降順になるよう並べ替えられる。

.by_group    第一引数に grouped_df 型のデータを入れたときの追加引数。もしも TRUE であれば、最初に所与の grouping variable でソートしてから式評価を開始する。
デフォルトは FALSE。

tidyなデータフレームの行ごとにデータを操作する

ここまでの関数はデータ内容には触らずに、ラベルを用いてデータフレームの行や列を弄る操作であった。いよいよ満を持して、データ内容自体の加工を開始する。

データフレームの行ごとにデータを操作する dplyr::mutate() 関数

データフレームが tidy である、すなわち個々の変数 (variable) が1つの列 (column) をなし、個々の観測 (observation) が1つの行 (row) をなすという条件を満たしていたとする。ここで、特定の1つ、またはそれ以上の変数に着目して、観測例ごとにデータの加工を行うのが mutate() 関数である。処理結果は通常、データフレームの新しい列として追加される。

例えば害虫の薬剤防除試験データの各観測に対して「補正密度指数 corrected density index」 \[ \mathrm{cdi} = \frac{N_{Treat,After}/N_{Treat,Before}}{N_{Control,After}/N_{Control,Before}} \] を計算する手続きは、以下のようになる。


> mutate(tidypest.merged, cdi=(Treat.After/Treat.Before)/(Control.After/Control.Before))
# A tibble: 12 x 8
   Season     Site Treat.Before Treat.After Control.Before Control.After Site.name     cdi
   <chr>     <int>        <int>       <int>          <int>         <int> <chr>       <dbl>
 1 August        1            6           4              4             1 Saitama     2.67
 2 August        2            2           6              5             0 Nagano    Inf
 3 August        3            4           1              5             0 Shizuoka  Inf
 4 August        4            1           6              3             0 Osaka     Inf
 5 August        5            2           5              4             0 Moscow    Inf
 6 August        6            2          NA              8             1 Melbourne  NA
 7 September     1            1           1              2             3 Saitama     0.667
 8 September     2            5           2              1             3 Nagano      0.133
 9 September     3            3           0              4             3 Shizuoka    0
10 September     4            2           0              1             5 Osaka       0
11 September     5            0           2              2             7 Moscow    Inf
12 September     6            5           1              4             5 Melbourne   0.160

基本的に cdi は(ゼロ除算が発生しなければ)非負の実数値を取り、ゼロに近いほど薬剤処理による害虫個体群への密度低減効果が高い(なお室内で虫の死亡率を直接はかる試験ではなく、野外の圃場で薬を使った前と後の、虫の獲れ具合を比較している)。


tidyなデータフレームの行ごとにデータを操作する
mutate(.data, ...)

省略不可な引数
.data    絞り込み対象とするテーブル形式のデータ。

...    行いたい処理。複数あればコンマで区切る。
(新しい列名)=(ここに処理内容) とすれば、.data に新しい列を追加できる。
(もともとあった列名)=(ここに処理内容) とすれば、元々 .data にあった列の内容を書き換えられる。
(もともとあった列名)=NULL としてヌルを代入すれば、.data から列を削除する操作になる。

データフレーム中の条件指定した列を一括で mutate する dplyr::mutate_at() 関数と dplyr::mutate_if() 関数

いずれもデータフレーム中の、複数の列に対して mutate に準ずる操作を行いたい場合に用いる。_at() と _if() の違いは、処理対象としたい列をユーザーが指定する方法の差である。

mutate_if() 関数では select() 関数と同様の方法で、第2引数の .predicates に条件処理を書き込むことで、処理対象列を絞り込むことができる。この条件処理は最終的な解釈結果として、対象データフレームの列の数と同じ長さの論理値ベクトル(TRUEとなる列が処理対象になる)を返す必要がある。

一方 mutate_at() は、処理対象としたい列(名前もしくはインデックス)だけを、第2引数 .vars を用いて直接的に与える。

とりあえず全部の列を処理したいんじゃいという場合、mutate_all() が使える。


データフレーム中の条件指定した列を一括で mutate する
mutate_all(.tbl, .funs, ...)
mutate_if(.tbl, .predicate, .funs, ...)
mutate_at(.tbl, .vars, .funs, ..., .cols = NULL)

全ての関数で省略不可な引数
.tbl    操作対象の列を含むテーブル形式のデータ。

.funs    対象の列に対して行う操作。リストにまとめられた関数呼び出し(funs() 関数で生成される)として与えるか、関数の名前を文字列ベクトルとして列挙する。もしくは単純に1つだけ関数を与える。
(以下直訳)複数の formula を裸で与えると rlang::as_function() へ渡され、purrr スタイルのラムダ式になる。こうして生成されるラムダ式においては hybrid evaluation from happening ができないため、ラムダ式で処理内容を与えるよりも mean() 等の関数を直接与えるほうが、計算効率は良い。

...    .funs の関数呼び出し時に与える追加の引数がもしあれば。いずれも一度しか評価されない。tidy-dots サポートあり。何のことやらという人は以下を見よ。
https://www.rdocumentation.org/packages/rlang/versions/0.2.1/topics/tidy-dots

関数特異的な引数

.predicate    mutate_if() 関数において、対象列を絞り込むための選択基準を与える。叙述関数(predicate function: bool型の評価結果を返す関数のこと)、もしくはどの列を選択する/しないをダイレクトに記述した logical vector。mutate_if() 関数においては、この .predicate が TRUE であるところの変数が処理の対象に選ばれる。
この引数は rlang::as_function() に渡されるため、quosure スタイルのラムダ式や、関数の名前を "" で括って文字列形式で与えるということも可能である。

.vars    mutate_at() 関数において、対象列を絞り込むための選択基準を与える。vars() 関数によって生成されるリスト、もしくは列の名を並べた文字列ベクトル、さらには何番目の列を対象とするかを示す数値ベクトルとして与えることもできる。

.cols    旧いバージョンの mutate_at() では .vars の替わりに .cols と呼んでいた。つまり .cols=c("hoge", "huga") などとしても対象列を指定できるが、dplyr の文法の統一性を図るため非推奨である。

まあ大雑把に使う場合は starts_with() とか、適当に例文をコピペして改造すればいいが、mutate_if() を本格的に使いこなそうとすると rlang パッケージの世界に突入してしまう。面倒なのである。後ついでに言うと、引数に列名(変数名)なんかを入れるとき、quote を付けるか付けないかは普段意識せずにやっていることだが、実はかなり複雑な内部処理が存在するらしい。たとえば dplyr再入門(Tidyeval編) speakerdeck.com など眺めてみるとよい。

処理対象列の処理後の姿だけをデータフレーム中に残しつつ mutate する dplyr::transmute() 関数

データフレームの特定の列に対して mutate 操作を行うと、出力結果には処理に使われなかった非対象列、および(同じ名前で代入されなければ)計算前の状態の対象列も保持される。これらのいずれも必要無いわい、という人はtransmute() という関数で、処理を加えた後の対象変数列だけを出力できる。mutate と同様に transmute_at() , transmute_if(), transmute_all() といった腰巾着もいる。


> mutate(band_instruments, sub=substring(name, 1, 2))
# A tibble: 3 x 3
  name  plays  sub
 <chr> <chr>   <chr>
1 John  guitar Jo
2 Paul  bass   Pa
3 Keith guitar Ke

> transmute(band_instruments, sub=substring(name, 1, 2))
# A tibble: 3 x 1
  sub
 <chr>
1 Jo
2 Pa
3 Ke

処理対象列の処理後の姿だけをデータフレーム中に残しつつ mutate する
transmute(.data, ...)
transmute_all(.tbl, .funs, ...)
transmute_if(.tbl, .predicate, .funs, ...)
transmute_at(.tbl, .vars, .funs, ..., .cols = NULL)

dplyr の関数における非標準評価のこと

前節において filter() の基本的な使用法自体に何ら難しいことは無いが、最後にヘルプドキュメントが、いきなり難しい概念をぶち込んできた。vignette("programming") というのは これのこと。冒頭を読むと「Most dplyr functions use non-standard evaluation (NSE). This is a catch-all term that means they don’t follow the usual R rules of evaluation.(ほとんどの dplyr の関数は非標準評価=要するに通常のRの式評価ルールに従わない)」だそうな。その代わりの利点として、関数のカッコ内でデータフレームの名前を繰り返し書く必要がなく、列名を書くだけでアクセスできる(バニラなRでは attach() する必要がある)。またRをデータベースに接続して使っている場合に、dplyrの関数そのものに仕事をさせるのではなく、外部データベースを操作するためのコマンドを生成させる手段として dplyr を使う、といった操作が容易となる。

ただし、引数の参照透過性(その式をその式の値に置き換えてもプログラムの振る舞いが変わらないこと)が失われるという欠点も伴う。


# この操作は、下の操作とは異なる。
filter(tidypest.merged, Site==1)

# A tibble: 2 x 7
  Season     Site Treat.Before Treat.After Control.Before Control.After Site.name
  <chr>     <int>        <int>       <int>          <int>         <int> <chr>
1 August        1            6           4              4             1 Saitama
2 September     1            1           1              2             3 Saitama

# "Site" という文字列を my_var に代入してから、my_var を指定してフィルターを掛ける操作
my_var <- "Site"
filter(tidypest.merged, my_var==1)

# A tibble: 0 x 7
# ... with 7 variables: Season <chr>, Site <int>, Treat.Before <int>, Treat.After <int>, Control.Before <int>, Control.After <int>, Site.name <chr>
つまりマッチする行がない

もし「参照透過性」があれば、filter()関数は my_var の指し示すところ、つまり "Site" にマッチさせたいのだなと解釈してくれる。ここではそのようなエスパーは起こらず、my_var なんて名前の列は tidypest.merged には無いから何もマッチングしません、という解釈をされてしまう。

また、以下のような曖昧さも生じてしまう。


たとえば、どの変数がどのスコープで定義されているかによって、 filter(df, x==y) は以下の4通りに解釈されうる。

df[df$x == df$y, ] # x, y がいずれも df の列名である。
df[df$x == y, ] # x は df の列名であるが、 y は df とは無関係のデータオブジェクトである。
df[x == df$y, ] # y は df の列名であるが、 x は df とは無関係のデータオブジェクトである。
df[x == y, ] # x, y ともに df とは無関係のベクトルオブジェクトであり、単に x 要素と y の同じ位置の要素が一致したか否かで判定される。

というわけで、これらの艱難辛苦に耐えて dplyr を手足のごとく使いこなすには、 "pronouns" および "quasiquatation" を用いてすっきり整理されたソースコードを書けるようになるとともに、"quosures" および "tidyeval" という概念を理解する必要があると vignette には書かれている。しかしこのページでそこまで説明するのは大変なので、そろそろ次の話題に移りたい。