まとめ
group, nest, map, mutateという4つの道具を使ってデータをハンドルする方法を紹介します。r-wakalangに寄せられる質問の中に、あー、これを知っていれば簡単に出来るのに!というモノが多いので、その際に「この記事読むといいかも!」という内容をまとめました(なのであまりふざけていない)。
レベルとしてはTokyo.Rの初心者セッションのデモンストレーションに使うぐらいのもので、それほど難しくないと思います。細かいテクを省いて、丁寧に追っていけば概念を理解できるように努めました。
【テンプレ冒頭句】
コードはご自分の手元で、コピペではなく、手打ちで実行しながら確かめてみることをお勧めします。
また、発展編についてはいくつか記事を書いているので、そちらもご参照ください。
・mapについてもう少し詳しく:{purrr} mapを導入しよう。
・nestについてもう少し詳しく:{tidyr} nestしていこう。
・合わせ技一本をもう少し詳しく:{lme4} 線形混合モデルの取り回し
・R自体やtidyverseの入門について:R言語入門(裏口)-- Landscape with R --
準備
library(tidyverse)
set.seed(71)
tidyverse
パッケージを読み込んでおく。
乱数を使うので再現性を担保するためにseedを固定する。
サンプルデータ
N <- 15
dat <- tibble(tag1 = sample(LETTERS[1:3], N, replace = TRUE),
tag2 = sample(letters[1:5], N, replace = TRUE),
y = rnorm(N),
x = runif(N))
先頭を表示してデータセットを確認してみます。
head(dat)
## # A tibble: 6 x 4
## tag1 tag2 y x
## <chr> <chr> <dbl> <dbl>
## 1 C d -1.15 0.449
## 2 C e 0.0100 0.808
## 3 B e 2.37 0.239
## 4 A a 0.324 0.0138
## 5 C e -0.887 0.0279
## 6 B c -0.730 0.161
#pipeの挙動
パイプ演算子については何度か書いていますので、挙動を紹介するだけに留めます。
#mutateの挙動
mutate()
関数は、カラム編集用の関数です。
基本的には既存のカラムを材料として使って、新たなカラムを作る時に使います。
dat %>%
mutate(val = 3 * x + 1)
## # A tibble: 15 x 5
## tag1 tag2 y x val
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 C d -1.15 0.449 2.35
## 2 C e 0.0100 0.808 3.42
## 3 B e 2.37 0.239 1.72
## 4 A a 0.324 0.0138 1.04
## 5 C e -0.887 0.0279 1.08
## 6 B c -0.730 0.161 1.48
複数のカラムを材料にすることもできます。
dat %>%
mutate(val = 3 * x + y)
関数をかませることもできます。
f <- function(A, B){ 3 * A + B }
dat %>%
mutate(val = f(x, y))
結果は一致するはずなので、試してみてください。
また、カラム名を既存のもので指定すると、結果は上書きされます。
dat %>%
group_by(tag1) %>%
mutate(val = 1)
## # A tibble: 15 x 5
## # Groups: tag1 [3]
## tag1 tag2 y x val
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 C d -1.15 0.449 1
## 2 C e 0.0100 0.808 1
## 3 B e 2.37 0.239 1
## 4 A a 0.324 0.0138 1
## 5 C e -0.887 0.0279 1
## 6 B c -0.730 0.161 1
dat %>%
mutate(val = 1) %>%
mutate(val = cumsum(val))
## # A tibble: 15 x 5
## tag1 tag2 y x val
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 C d -1.15 0.449 1
## 2 C e 0.0100 0.808 2
## 3 B e 2.37 0.239 3
## 4 A a 0.324 0.0138 4
## 5 C e -0.887 0.0279 5
## 6 B c -0.730 0.161 6
#group_byの挙動
group_by()
関数は、指定されたカラムをカテゴリカル変数として扱い、その水準をグループとしてまとめるという挙動をします。
dat %>%
group_by(tag1)
## # A tibble: 15 x 4
## # Groups: tag1 [3]
## tag1 tag2 y x
## <chr> <chr> <dbl> <dbl>
## 1 C d -1.15 0.449
## 2 C e 0.0100 0.808
## 3 B e 2.37 0.239
## 4 A a 0.324 0.0138
## 5 C e -0.887 0.0279
## 6 B c -0.730 0.161
一見、何も起こっていませんが、# Groups: tag1 [3]
が追加されていますね。
グループとしてまとめるというのはどういうことかというと、次のコードを試してみてください。
dat %>%
group_by(tag1) %>%
mutate(val = 1,
val = cumsum(val))
分かりづらければ、更にarrange()
関数で並べ替えてみましょう。
dat %>%
group_by(tag1) %>%
mutate(val = 1,
val = cumsum(val)) %>%
arrange(tag1)
## # A tibble: 15 x 5
## # Groups: tag1 [3]
## tag1 tag2 y x val
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 A a 0.324 0.0138 1
## 2 A c -0.904 0.414 2
## 3 A d 0.900 0.427 3
## 4 A d 1.29 0.555 4
## 5 B e 2.37 0.239 1
## 6 B c -0.730 0.161 2
group化しなかった時と見比べると、cumsum()
関数の適用範囲がグループ内に留まっていることがわかります。これは厳密に挙動して、たとえばlag()
関数のようなものでも使えます。以下を試してみてください。
dat %>%
group_by(tag1) %>%
mutate(val = 1,
val = cumsum(val)) %>%
arrange(tag1) %>%
mutate(val = lag(val))
group化は、summarise()
関数と相性が良く、グループ毎の要約統計量を抽出する際にも便利です。
dat %>%
summarise(x_mean = mean(x),
y_mean = mean(y))
## # A tibble: 1 x 2
## x_mean y_mean
## <dbl> <dbl>
## 1 0.411 -0.0386
dat %>%
group_by(tag1) %>%
summarise(x_mean = mean(x),
y_mean = mean(y))
## # A tibble: 3 x 3
## tag1 x_mean y_mean
## <chr> <dbl> <dbl>
## 1 A 0.352 0.402
## 2 B 0.482 0.210
## 3 C 0.390 -0.539
複数のカラムを引数に取ることもできます。以下を試してみて下さい。
dat %>%
group_by(tag1, tag2) %>%
summarise(x_mean = mean(x),
y_mean = mean(y))
グループ化することで、水準毎の処理が可能になりました。
ただし、summarise()
関数での処理が不可逆であることに注意しておいて下さい。
#nestの挙動
nest()
関数はデータを畳み込む挙動をします。
dat %>%
nest(.key = "data")
## # A tibble: 1 x 1
## data
## <list>
## 1 <tibble [15 × 4]>
もとのdat
はtibble: 15 x 4
でしたが、この結果はtibble: 1 x 1
になっています。
新しく作られたdata
カラムに1つのobsがあり、<tibble [15 × 4]>
がlist
として入っています。この新しいカラムはデフォルトではdata
という名前ですが、変更したいときは、.key
引数で指定します。
中身を見てみましょう。
dat %>%
nest() %>%
.$data
## [[1]]
## # A tibble: 15 x 4
## tag1 tag2 y x
## <chr> <chr> <dbl> <dbl>
## 1 C d -1.15 0.449
## 2 C e 0.0100 0.808
## 3 B e 2.37 0.239
## 4 A a 0.324 0.0138
## 5 C e -0.887 0.0279
## 6 B c -0.730 0.161
[[1]]
に注意して下さい。
これはtibble
を要素に取ったlist
形式でデータが保持されていて、ということです。
list形式についてはmapの章も参考にして下さい。
事前にgroup化しておくことで、水準に応じてデータを畳み込むことができます。
dat %>%
group_by(tag1) %>%
nest()
## # A tibble: 3 x 2
## tag1 data
## <chr> <list>
## 1 A <tibble [4 × 3]>
## 2 B <tibble [5 × 3]>
## 3 C <tibble [6 × 3]>
dat %>%
group_by(tag1, tag2) %>%
nest()
## # A tibble: 9 x 3
## tag1 tag2 data
## <chr> <chr> <list>
## 1 C d <tibble [1 × 2]>
## 2 C e <tibble [3 × 2]>
## 3 B e <tibble [2 × 2]>
## 4 A a <tibble [1 × 2]>
## 5 B c <tibble [2 × 2]>
## 6 B b <tibble [1 × 2]>
## 7 A c <tibble [1 × 2]>
## 8 A d <tibble [2 × 2]>
## 9 C b <tibble [2 × 2]>
このgroup_by
からのnest
を一度に行うgroup_nest()
関数も用意されています。
dat %>%
group_nest(tag1)
作られたdata
カラムの中身はこのようになっています。
dat %>%
group_nest(tag1, tag2) %>%
.$data
## [[1]]
## # A tibble: 1 x 2
## y x
## <dbl> <dbl>
## 1 0.324 0.0138
##
## [[2]]
畳み込まれたデータを広げる場合はunnest()
関数を使います。
以下を試してみて下さい。
dat %>%
group_nest(tag1, tag2) %>%
unnest()
mapの挙動
map()
関数とその一族は、list形式のデータを処理するためのものです。
特に、listの要素それぞれに対して、特定の関数を作用させた結果が欲しい時に使います。
my_list <- list(1:2, 3:5)
## [[1]]
## [1] 1 2
##
## [[2]]
## [1] 3 4 5
標的となるlistは.x
引数に、作用させたい関数は.f
引数に入れます(引数名は省略可能)。
map(.x = my_list, .f = sum)
## [[1]]
## [1] 3
##
## [[2]]
## [1] 12
自分で作った関数を.f
にとることもできます。
f <- function(x){ x + 1 }
my_list %>%
map(f)
map()
関数の中で関数を定義することもできます。
my_list %>%
map(function(x){ x + 1 })
作用させる関数をチルダ~
を使って無名関数として定義することも可能です。
my_list %>%
map( ~ { . + 1 })
このピリオド.
に、listの要素がそれぞれ代入されることになります。
2つのlistを引数にとるmap2()
関数も用意されています。
x1 <- list(a = 1:6, b = 3:8)
x2 <- list(a = 6:1, b = 10:15)
map2(x1, x2, ~ { .x + .y })
## $a
## [1] 7 7 7 7 7 7
##
## $b
## [1] 13 15 17 19 21 23
無名関数の中では、1つめのlistの要素は.x
、2つめのlistの要素は.y
にそれぞれ代入されます。当然、入力される2つのlistの要素の数は揃っている必要があります。
map()
やmap2()
を使うとlistの要素ごとに同一の関数を作用させることができました。
出力もまた、入力と同じ長さのlistであることに注意して下さい。
group → nest → map → mutate
さて、道具が整いました。
group化しておいてからnestしたデータは、list形式で畳み込まれているわけですが、ここにmapを使って特定の関数を作用させて、その結果をmutateで新しいカラムに格納していきます。
まずは復習。
dat_nest <- dat %>%
group_nest(tag1)
## # A tibble: 3 x 2
## tag1 data
## <chr> <list>
## 1 A <tibble [4 × 3]>
## 2 B <tibble [5 × 3]>
## 3 C <tibble [6 × 3]>
この構造を壊さずに、dataの中のxの平均値を計算してみます。
dat_nest %>%
mutate(x_mean = map(data, ~mean(.$x)))
## # A tibble: 3 x 3
## tag1 data x_mean
## <chr> <list> <list>
## 1 A <tibble [4 × 3]> <dbl [1]>
## 2 B <tibble [5 × 3]> <dbl [1]>
## 3 C <tibble [6 × 3]> <dbl [1]>
mutate()
関数によって作られた新しいカラムx_mean
には、それぞれ実数(dbl
)が1つずつ格納されているのがわかります。こんなときは、map()
関数のwrapperであるmap_dbl()
を使うと綺麗にできます。
dat_nest %>%
mutate(x_mean = map_dbl(data, ~mean(.$x)))
## # A tibble: 3 x 3
## tag1 data x_mean
## <chr> <list> <dbl>
## 1 A <tibble [4 × 3]> 0.352
## 2 B <tibble [5 × 3]> 0.482
## 3 C <tibble [6 × 3]> 0.390
map_dbl()
関数を使うことで、x_mean
カラムの形式がlistからdblに変わりましたね。
group_byからのsummariseでx_mean
を計算した時と比べて、元のデータを保持しているのがポイントです。
こうしておくと、スッキリと統計的検定も可能です。
dat_nest %>%
mutate(ttest = map(data, ~t.test(.$x, .$y)),
pval = map_dbl(ttest, ~{ .$p.value }))
## # A tibble: 3 x 4
## tag1 data ttest pval
## <chr> <list> <list> <dbl>
## 1 A <tibble [4 × 3]> <htest> 0.926
## 2 B <tibble [5 × 3]> <htest> 0.745
## 3 C <tibble [6 × 3]> <htest> 0.0456
部分を取り出して検定することも、見通しよくできます。
dat_nest %>%
mutate(x = map(data, ~filter(., tag2 != "b") %>% .$x),
y = map(data, ~filter(., tag2 != "b") %>% .$y),
ttest = map2(x, y, ~t.test(.x, .y)),
pval = map_dbl(ttest, ~{ .$p.value }))
## # A tibble: 3 x 6
## tag1 data x y ttest pval
## <chr> <list> <list> <list> <list> <dbl>
## 1 A <tibble [4 × 3]> <dbl [4]> <dbl [4]> <htest> 0.926
## 2 B <tibble [5 × 3]> <dbl [4]> <dbl [4]> <htest> 0.837
## 3 C <tibble [6 × 3]> <dbl [4]> <dbl [4]> <htest> 0.0311
このコードが「あーはいはい」と読めれば、あなたは既にmap脳です。
Appendix
併せて読みたい
冒頭にも書きましたが、それぞれもう少し詳しくは別記事を書いているのであわせてご参照ください。
・mapについてもう少し詳しく:{purrr} mapを導入しよう。
・nestについてもう少し詳しく:{tidyr} nestしていこう。
・合わせ技一本をもう少し詳しく:{lme4} 線形混合モデルの取り回し
・R自体やtidyverseの入門について:R言語入門(裏口)-- Landscape with R --