StatsFragments

Python, R, Rust, 統計, 機械学習とか

R dplyr, tidyr でのグルーピング/集約/変換処理まとめ

これの続き。よく使う集約/変換処理もまとめておく。

準備

library(dplyr)
library(tidyr)

(df <- dplyr::tbl_df(iris))
# Source: local data frame [150 x 5]
# 
#    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
# 3           4.7         3.2          1.3         0.2  setosa
# ..          ...         ...          ...         ...     ...

グルーピング/集約

ある列の値ごとに集計

Species 列ごとに Sepal.Length 列の合計を算出する場合、

df %>%
  dplyr::group_by(Species) %>%
  dplyr::summarise(Sepal.Length.Total = sum(Sepal.Length))
# Source: local data frame [3 x 2]
# 
#      Species Sepal.Length.Total
# 1     setosa              250.3
# 2 versicolor              296.8
# 3  virginica              329.4

# Standard Evaluation
df %>%
  dplyr::group_by_('Species') %>%
  dplyr::summarise_('Sepal.Length.Total' = 'sum(Sepal.Length)')
# 略

同一の集計を 複数列に対して行う場合は dplyr::summarise_eachが便利。全列の合計を取得するなら、

df %>%
  dplyr::group_by(Species) %>%
  dplyr::summarise_each(dplyr::funs(sum))
# Source: local data frame [3 x 5]
# 
#      Species Sepal.Length Sepal.Width Petal.Length Petal.Width
# 1     setosa        250.3       171.4         73.1        12.3
# 2 versicolor        296.8       138.5        213.0        66.3
# 3  virginica        329.4       148.7        277.6       101.3

vars 引数で集計対象列を指定することもできる。通常版 (Non-standard evaluation/NSE) と Standard evaluation で列名のつき方が違う、、、。

df %>%
  dplyr::group_by(Species) %>%
  dplyr::summarise_each(dplyr::funs(sum),
  vars = c(Petal.Width = Petal.Width, Petal.Length = Petal.Length))
# Source: local data frame [3 x 3]
# 
#      Species vars.Petal.Width vars.Petal.Length
# 1     setosa             12.3              73.1
# 2 versicolor             66.3             213.0
# 3  virginica            101.3             277.6

# Standard evaluation
df %>%
  dplyr::group_by_('Species') %>%
  dplyr::summarise_each_(dplyr::funs(sum), vars = c('Petal.Width', 'Petal.Length'))
# Source: local data frame [3 x 5]
# 
#      Species Petal.Width_sum Petal.Length_sum Petal.Width_mean Petal.Length_mean
# 1     setosa            12.3             73.1            0.246             1.462
# 2 versicolor            66.3            213.0            1.326             4.260
# 3  virginica           101.3            277.6            2.026             5.552

関数名を文字列渡しする場合は dplyr::funs_

df %>%
  dplyr::group_by(Species) %>%
  dplyr::summarise_each(dplyr::funs_('sum'),
                        vars=c(Petal.Width = Petal.Width, Petal.Length = Petal.Length))
# 略

# Standard evaluation
df %>%
  dplyr::group_by_('Species') %>%
  dplyr::summarise_each_(dplyr::funs_('sum'), vars=c('Petal.Width', 'Petal.Length'))
# 略

また、集約関数は複数渡すこともできる。

df %>%
  dplyr::group_by(Species) %>%
  dplyr::summarise_each(funs = c(dplyr::funs(sum), dplyr::funs(mean)),
                         vars=c(Petal.Width = Petal.Width, Petal.Length = Petal.Length))
# Source: local data frame [3 x 5]
# 
#      Species vars.Petal.Width_sum vars.Petal.Length_sum vars.Petal.Width_mean vars.Petal.Length_mean
# 1     setosa                 12.3                  73.1                 0.246                  1.462
# 2 versicolor                 66.3                 213.0                 1.326                  4.260
# 3  virginica                101.3                 277.6                 2.026                  5.552

# Standard evaluation
df %>%
  dplyr::group_by_('Species') %>%
  dplyr::summarise_each_(funs = c(dplyr::funs(sum), dplyr::funs(mean)),
                         vars=c('Petal.Width', 'Petal.Length'))
# Source: local data frame [3 x 5]
# 
#      Species Petal.Width_sum Petal.Length_sum Petal.Width_mean Petal.Length_mean
# 1     setosa            12.3             73.1            0.246             1.462
# 2 versicolor            66.3            213.0            1.326             4.260
# 3  virginica           101.3            277.6            2.026             5.552

NSEの dplyr::groupby に文字列を渡した場合、特にエラーが出ず全集約されるので注意。

# NG!
df %>%
  dplyr::group_by('Species') %>%
  dplyr::summarise_each_(funs = c(dplyr::funs(sum), dplyr::funs(mean)),
                         vars=c('Petal.Width', 'Petal.Length'))
# Source: local data frame [1 x 5]
# 
#   "Species" Petal.Width_sum Petal.Length_sum Petal.Width_mean Petal.Length_mean
# 1   Species           179.9            563.7         1.199333             3.758

行持ち / 列持ち変換

複数列持ちの値を行持ちに展開 (unpivot / melt)

複数列で持っている値を行持ちに展開する処理は、reshape2::melt から名前が変わって tidyr::gather になった。この処理の名前として一般的なのは unpivot ではないのか、、。

tidyr でも Standard evaluationが使える。

df %>%
  tidyr::gather(variable, value, c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width))
# Source: local data frame [600 x 3]
# 
#    Species     variable value
# 1   setosa Sepal.Length   5.1
# 2   setosa Sepal.Length   4.9
# 3   setosa Sepal.Length   4.7
# ..     ...          ...   ...

# Standard evaluation
df %>%
  tidyr::gather_('variable', 'value', c('Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width'))
# 略

複数行持ちの値を列持ちに変換 (pivot)

tidyr::spreadspreadでは集計処理は入らない (行列の入替のみ) ので、まず 行/列として展開する 2列がユニークとなるサンプルデータを用意する。

# spreadへの入力データ。Species (列にする値) と variable (行にする値) の組がユニークでないとダメ。
(unpivot <- df %>%
  tidyr::gather(variable, value, c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)) %>%
  dplyr::group_by(Species, variable) %>%
  dplyr::summarise(Total = sum(value)))
# Source: local data frame [12 x 3]
# Groups: Species
# 
#       Species     variable Total
# 1      setosa Sepal.Length 250.3
# 2      setosa  Sepal.Width 171.4
# 3      setosa Petal.Length  73.1
# 4      setosa  Petal.Width  12.3
# 5  versicolor Sepal.Length 296.8
# 6  versicolor  Sepal.Width 138.5
# 7  versicolor Petal.Length 213.0
# 8  versicolor  Petal.Width  66.3
# 9   virginica Sepal.Length 329.4
# 10  virginica  Sepal.Width 148.7
# 11  virginica Petal.Length 277.6
# 12  virginica  Petal.Width 101.3

unpivot %>%
  data.frame %>%
  tidyr::spread(Species, Total)
#       variable setosa versicolor virginica
# 1 Sepal.Length  250.3      296.8     329.4
# 2  Sepal.Width  171.4      138.5     148.7
# 3 Petal.Length   73.1      213.0     277.6
# 4  Petal.Width   12.3       66.3     101.3

# Standard evaluation
unpivot %>%
  data.frame %>%
  tidyr::spread_('Species', 'Total')
# 略

(たぶんバグだと思うが) grouped_dfspread に渡すと以下の通りエラーになるので、上では spread の前に data.frame への変換を挟んでいる。

→ 10/16 追記: バグでしたdplyr 3.1 で修正予定。

class(unpivot)
# [1] "grouped_df" "tbl_df"     "tbl"        "data.frame"

# NG!
unpivot %>%
  tidyr::spread(Species, Total)
#  エラー: index out of bounds

列の分割 / 結合

列の値を複数列に分割

上で作ったデータから variable 列の値をドットで区切って二列に分けたい、なんてときには tidyr::separate。NSE の場合も into に渡す値は文字列 vector でないとダメっぽい (新しい列名を作る処理になるからかなあ、 dplyr::rename では作成する列名もシンボルで渡せるのに)。

df %>%
  tidyr::gather(variable, value, c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)) %>%
  tidyr::separate(variable, into = c('Parts', 'Scale'), sep='\\.')
# Source: local data frame [600 x 4]
# 
#    Species Parts  Scale value
# 1   setosa Sepal Length   5.1
# 2   setosa Sepal Length   4.9
# 3   setosa Sepal Length   4.7
# ..     ...   ...    ...   ...

# Standard evaluation
df %>%
  tidyr::gather_('variable', 'value', c('Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width')) %>%
  tidyr::separate_('variable', into = c('Parts', 'Scale'), sep='\\.')
# 略

正規表現でグルーピングした値で列分割したい場合は tidyr::extract

df %>%
  tidyr::gather(variable, value, c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)) %>%
  tidyr::extract(variable, into = c('Parts', 'Scale'), regex='(.+)\\.(.+)')
# Source: local data frame [600 x 4]
# 
#    Species Parts  Scale value
# 1   setosa Sepal Length   5.1
# 2   setosa Sepal Length   4.9
# 3   setosa Sepal Length   4.7
# ..     ...   ...    ...   ...

# Standard evaluation
df %>%
  tidyr::gather_('variable', 'value', c('Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width')) %>%
  tidyr::extract_('variable', into = c('Parts', 'Scale'), regex='(.+)\\.(.+)')
# 略

複数列の値を一列に結合

tidyr::separateと逆の操作はtidyr::uniteでできる。

df %>%
  tidyr::gather(variable, value, c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)) %>%
  tidyr::separate(variable, into = c('Parts', 'Scale'), sep='\\.') %>%
  tidyr::unite('variable', c(Parts, Scale), sep='.')
# Source: local data frame [600 x 3]
# 
#    Species     variable value
# 1   setosa Sepal.Length   5.1
# 2   setosa Sepal.Length   4.9
# 3   setosa Sepal.Length   4.7
# ..     ...          ...   ...  

# Standard evaluation
df %>%
  tidyr::gather_('variable', 'value', c('Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width')) %>%
  tidyr::separate_('variable', into = c('Parts', 'Scale'), sep='\\.') %>%
  tidyr::unite_('variable', c('Parts', 'Scale'), sep='.')
# 略