52
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

map脳になろう、もしくはnested dataのハンドリング

Last updated at Posted at 2019-11-19

まとめ

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]>

もとのdattibble: 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 --

Enjoy!!

52
52
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?