StatsFragments

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

{purrr} でリストデータを操作する <2>

前の記事に続けて、{purrr}{rlist} 相当の処理を行う。今回はレコードの選択とソート。

sinhrks.hatenablog.com

サンプルデータは前回と同じものを利用する。リストの表示も同じく Hmisc::list.tree を使う。

library(rlist)
library(pipeR)
library(purrr)

packages <- list(
  list(name = 'dplyr', star = 979L, maintainer = 'hadley' , authors = c('hadley', 'romain')),
  list(name = 'ggplot2', star = 1546L, maintainer = 'hadley' , authors = c('hadley')),
  list(name = 'knitr', star = 1047L, maintainer = 'yihui' , authors = c('yihui', 'hadley', '...and all'))
)
packages
##  x = list 3 (2632 bytes)
## .  [[1]] = list 4
## . .  name = character 1= dplyr 
## . .  star = integer 1= 979
## . .  maintainer = character 1= hadley 
## . .  authors = character 2= hadley romain 
## .  [[2]] = list 4
## . .  name = character 1= ggplot2 
## . .  star = integer 1= 1546
## . .  maintainer = character 1= hadley 
## . .  authors = character 1= hadley 
## .  [[3]] = list 4
## . .  name = character 1= knitr 
## . .  star = integer 1= 1047
## . .  maintainer = character 1= yihui 
## . .  authors = character 3= yihui hadley  ...

レコードの選択

rlist Tutorial: Filtering 後半の内容。

ある条件にあてはまるレコードのうち最初の N 件だけを取得したいとき、{rlist} には専用の関数 list.find がある。{purrr} には対応する関数はないが、keep %>% head で同じ結果が得られる。

packages %>>% rlist::list.find(star >= 1000, 1)
##  x = list 1 (840 bytes)
## .  [[1]] = list 4
## . .  name = character 1= ggplot2 
## . .  star = integer 1= 1546
## . .  maintainer = character 1= hadley 
## . .  authors = character 1= hadley

packages %>%
  purrr::keep(~ .$star >= 1000) %>%
  head(n = 1)
# 略

また、{rlist} には list.findi という インデックスを返す関数がある。purrr::keep では、引数に logical 型のベクトルを渡せば TRUE に対応する要素のみを残すことができる。

packages %>>% rlist::list.findi(star >= 1000, 2)
## [1] 2 3

purrr::keep(seq_along(packages),
            flatmap(packages, ~ .$star >= 1000)) %>%
  head(n = 2)
# 略

条件にあてはまる最初のレコードだけを取得したいときは rlist::first もしくは purrr::detect

rlist::list.first(packages, star >= 1000)
##  x = list 4 (792 bytes)
## .  name = character 1= ggplot2 
## .  star = integer 1= 1546
## .  maintainer = character 1= hadley 
## .  authors = character 1= hadley

purrr::detect(packages, ~ .$star >= 1000)
# 略

レコード数がそこまで多くない場合は keep %>% head(n = 1) でよいと思うが、結果に 1 レベル目が入れ子として残ってしまう。上と同じ結果を得るには purrr::flatten ( unlist(recursive = FALSE) と同じ ) を通す。

purrr::keep(packages, ~ .$star >= 1000) %>% head(n = 1)
##  x = list 1 (840 bytes)
## .  [[1]] = list 4
## . .  name = character 1= ggplot2 
## . .  star = integer 1= 1546
## . .  maintainer = character 1= hadley 
## . .  authors = character 1= hadley

purrr::keep(packages, ~ .$star >= 1000) %>%
  head(n = 1) %>%
  purrr::flatten()
##  x = list 4 (792 bytes)
## .  name = character 1= ggplot2 
## .  star = integer 1= 1546
## .  maintainer = character 1= hadley 
## .  authors = character 1= hadley

同じく、最後のレコードを取得したいときは rlist::last もしくは purrr::detect(.right = TRUE)。これも自分としては keep %>% tail(n = 1) のほうが覚えやすい。

list.last(packages, star >= 1000)
##  x = list 4 (920 bytes)
## .  name = character 1= knitr 
## .  star = integer 1= 1047
## .  maintainer = character 1= yihui 
## .  authors = character 3= yihui hadley  ...

purrr::detect(packages, ~ .$star >= 1000, .right = TRUE)
# 略

purrr::keep(packages, ~ .$star >= 1000) %>%
  tail(n = 1) %>%
  purrr::flatten()
# 略

類似の処理として、{rlist} には list.take という最初の N レコードを取得する関数と list.skip という最初の N レコードを除外する関数がある。{purrr} には対応する関数はないが、組み込みの headtail でよいのでは...。

packages %>>% rlist::list.take(2)
##  x = list 2 (1696 bytes)
## .  [[1]] = list 4
## . .  name = character 1= dplyr 
## . .  star = integer 1= 979
## . .  maintainer = character 1= hadley 
## . .  authors = character 2= hadley romain 
## .  [[2]] = list 4
## . .  name = character 1= ggplot2 
## . .  star = integer 1= 1546
## . .  maintainer = character 1= hadley 
## . .  authors = character 1= hadley

packages %>% head(n = 2)
# 略

packages %>% rlist::list.skip(1)
##  x = list 2 (1768 bytes)
## .  [[1]] = list 4
## . .  name = character 1= ggplot2 
## . .  star = integer 1= 1546
## . .  maintainer = character 1= hadley 
## . .  authors = character 1= hadley 
## .  [[2]] = list 4
## . .  name = character 1= knitr 
## . .  star = integer 1= 1047
## . .  maintainer = character 1= yihui 
## . .  authors = character 3= yihui hadley  ...

packages %>% tail(length(.) - 1)
# 略

ある条件が初めて偽となるまでレコードを取得する場合は rlist::list.takeWhile もしくは purrr::head_while

packages %>>% rlist::list.takeWhile(star <= 1500)
##  x = list 1 (896 bytes)
## .  [[1]] = list 4
## . .  name = character 1= dplyr 
## . .  star = integer 1= 979
## . .  maintainer = character 1= hadley 
## . .  authors = character 2= hadley romain

packages %>% purrr::head_while(~ .$star <= 1500)
# 略

ある条件が初めて偽となったレコード以降を取得したい場合は rlist::list.skipWhile{purrr} には対応する処理はないが、purrr::detect_index を使って以下のように書ける。

packages %>>% rlist::list.skipWhile(star <= 1500)
##  x = list 2 (1768 bytes)
## .  [[1]] = list 4
## . .  name = character 1= ggplot2 
## . .  star = integer 1= 1546
## . .  maintainer = character 1= hadley 
## . .  authors = character 1= hadley 
## .  [[2]] = list 4
## . .  name = character 1= knitr 
## . .  star = integer 1= 1047
## . .  maintainer = character 1= yihui 
## . .  authors = character 3= yihui hadley  ...

purrr::detect_index(packages, ~ .$star > 1500)
## [1] 2
packages[purrr::detect_index(packages, ~ .$star > 1500):length(packages)]
# 略 

一方、{purrr} には tail_while という末尾からみて条件にあてはまるレコードだけを取得する関数がある。

packages %>% purrr::tail_while(~ .$star <= 1500)
##  x = list 1 (968 bytes)
## .  [[1]] = list 4
## . .  name = character 1= knitr 
## . .  star = integer 1= 1047
## . .  maintainer = character 1= yihui 
## . .  authors = character 3= yihui hadley  ...

ある条件を満たす要素がいくつあるかを調べるには rlist::list.count{purrr} なら keep %>% length でよい。

list.count(packages, star > 1000)
## [1] 2

purrr::keep(packages, ~ .$star > 1000) %>% length()
# 略

{rlist} には リストの要素の名前を正規表現で抽出する list.match がある。{purrr} なら keep でできる。stringr::str_match はマッチした結果を matrix で返すため、complete.caseslogical に変換する。

data <- list(p1 = 1, p2 = 2, a1 = 3, a2 = 4)
list.match(data, "p[12]")
##  x = list 2 (416 bytes)
## .  p1 = double 1= 1
## .  p2 = double 1= 2

complete.cases(stringr::str_match(names(data), 'p[12]'))
## [1]  TRUE  TRUE FALSE FALSE
keep(data, complete.cases(stringr::str_match(names(data), 'p[12]')))
# 略

logical の処理

条件に当てはまるかどうかを logical ベクトルとして返すには rlist::list.is。これは rlist::list.mapv と同じなのでは...? {purrr} なら flatmap もしくは map_lgl

packages %>>% rlist::list.is(star < 1500)
## [1]  TRUE FALSE  TRUE

packages %>>% rlist::list.mapv(star < 1500)
# 略

packages %>% purrr::flatmap(~ .$star < 1500)
# 略

packages %>% purrr::map_lgl(~ .$star < 1500)
# 略

また、logical のリスト全体の真偽値を以下のように取得できる

  • 要素全てが TRUE かどうか: rlist::list.all もしくは purrr::every
  • 要素いずれかが TRUE かどうか: rlist::list.any もしくは purrr::some

自分は purrr::flatmap %>% all もしくは purrr::flatmap %>% any のほうが覚えやすい。

ただし、{purrr} v0.1.0 の every, some にはバグがあり正しく動作しない。下のスクリプトを実行するためには GitHub から最新の master をインストールする必要がある。

rlist::list.all(packages, star >= 500)
## [1] TRUE

# install_github('hadley/purrr') # が必要
purrr::every(packages, ~ .$star >= 500) 
# 略

packages %>% purrr::flatmap(~ .$star >= 500) %>% all()
# 略

rlist::list.any(packages, star >= 1500)
## [1] TRUE

# install_github('hadley/purrr') # が必要
purrr::some(packages, ~ .$star >= 1500)

packages %>% purrr::flatmap(~ .$star >= 1500) %>% any()
# 略

要素の削除

{rlist} では list.remove で指定した名前をリストから除外できる。{purrr} には discard という条件にマッチする要素をリストから除外する関数がある ( purrr::keep とは逆 )。条件の否定をとれば keep でも書ける。

rlist::list.remove(data, c("p1", "p2"))
##  x = list 2 (416 bytes)
## .  a1 = double 1= 3
## .  a2 = double 1= 4

purrr::discard(data, names(data) %in% c("p1", "p2"))
# 略

purrr::keep(data, !(names(data) %in% c("p1", "p2")))
# 略

{rlist} で、名前ではなく 条件式を使ってリストから除外したい場合は list.exclude{purrr} の場合は 上と同じく discard もしくは keep

packages %>>% rlist::list.exclude("yihui" %in% authors)
##  x = list 2 (1696 bytes)
## .  [[1]] = list 4
## . .  name = character 1= dplyr 
## . .  star = integer 1= 979
## . .  maintainer = character 1= hadley 
## . .  authors = character 2= hadley romain 
## .  [[2]] = list 4
## . .  name = character 1= ggplot2 
## . .  star = integer 1= 1546
## . .  maintainer = character 1= hadley 
## . .  authors = character 1= hadley

packages %>% purrr::discard(~ "yihui" %in% .$authors)
# 略

また、{rlist} には 欠損などの異常値を リストから除去する list.clean という関数がある。下の例では リストの 1 レベル目にある欠損 "b" を除去している。{purrr} では同じく discard

x <- list(a = 1, b = NULL, c = list(x = 1, y = NULL, z = logical(0L), w = c(NA, 1)))
x
##  x = list 3 (1040 bytes)
## .  a = double 1= 1
## .  b = NULL 0
## .  c = list 4
## . .  x = double 1= 1
## . .  y = NULL 0
## . .  z = logical 0
## . .  w = double 2= NA 1

rlist::list.clean(x)
##  x = list 2 (960 bytes)
## .  a = double 1= 1
## .  c = list 4
## . .  x = double 1= 1
## . .  y = NULL 0
## . .  z = logical 0
## . .  w = double 2= NA 1

purrr::discard(x, ~ is.null(.))
# 略

rlist::list.cleanrecursive = TRUE を指定することで 処理を再帰的に適用できる。{purrr} ではこういった再帰的な処理は (自分で関数を書かない限り) できないと思う。

rlist::list.clean(x, recursive = TRUE)
##  x = list 2 (912 bytes)
## .  a = double 1= 1
## .  c = list 3
## . .  x = double 1= 1
## . .  z = logical 0
## . .  w = double 2= NA 1

要素の更新

rlist Tutorial: Updating に相当する内容。

rlist::list.mappurrr::map を用いたリストの選択では、結果に含める属性を明示的に指定する必要があった。 これは対象のリストに選択したい属性が多い場合はすこし面倒だ。

rlist::list.update を使うと、もともとのリストの属性を残した上で 指定した要素を追加 / 更新できる。{purrr} で同じことをするには map + update_list

packages %>>% rlist::list.update(lang = 'R', star = star + 1)
##  x = list 3 (3160 bytes)
## .  [[1]] = list 5
## . .  name = character 1= dplyr 
## . .  star = double 1= 980
## . .  maintainer = character 1= hadley 
## . .  authors = character 2= hadley romain 
## . .  lang = character 1= R 
## .  [[2]] = list 5
## . .  name = character 1= ggplot2 
## . .  star = double 1= 1547
## . .  maintainer = character 1= hadley 
## . .  authors = character 1= hadley 
## . .  lang = character 1= R 
## .  [[3]] = list 5
## . .  name = character 1= knitr 
## . .  star = double 1= 1048
## . .  maintainer = character 1= yihui 
## . .  authors = character 3= yihui hadley  ... 
## . .  lang = character 1= R

packages %>% purrr::map(~ purrr::update_list(., lang = 'R', star = ~ .$star + 1))
# 略

一部の要素を取り除きたい場合は NULL を指定する。これは {purrr} も同じ。

packages %>>% rlist::list.update(maintainer = NULL, authors = NULL)
##  x = list 3 (1464 bytes)
## .  [[1]] = list 2
## . .  name = character 1= dplyr 
## . .  star = integer 1= 979
## .  [[2]] = list 2
## . .  name = character 1= ggplot2 
## . .  star = integer 1= 1546
## .  [[3]] = list 2
## . .  name = character 1= knitr 
## . .  star = integer 1= 1047

packages %>% purrr::map(~ purrr::update_list(., maintainer = NULL, authors = NULL))
# 略

ソート

rlist Tutorial: Sorting に相当する内容。

{rlist}, {purrr} とも 標準の sort ( 値のソート ) と order ( インデックスのソート ) それぞれに対応した関数を持つ。

x = c('a', 'c', 'd', 'b')

sort(x)
## [1] "a" "b" "c" "d"

order(x)
## [1] 1 4 2 3

sort のようにソートした値を得るためには rlist::list.sort もしくは purrr::sort_by

packages %>>% rlist::list.sort(star)
##  x = list 3 (2632 bytes)
## .  [[1]] = list 4
## . .  name = character 1= dplyr 
## . .  star = integer 1= 979
## . .  maintainer = character 1= hadley 
## . .  authors = character 2= hadley romain 
## .  [[2]] = list 4
## . .  name = character 1= knitr 
## . .  star = integer 1= 1047
## . .  maintainer = character 1= yihui 
## . .  authors = character 3= yihui hadley  ... 
## .  [[3]] = list 4
## . .  name = character 1= ggplot2 
## . .  star = integer 1= 1546
## . .  maintainer = character 1= hadley 
## . .  authors = character 1= hadley

packages %>% purrr::sort_by('star')
# 略

{rlist} では 列名を括弧でくくると逆順となる。{purrr} には対応する記法がないが、結果を rev で逆順にすればよい。

packages %>>% rlist::list.sort((star))
##  x = list 3 (2632 bytes)
## .  [[1]] = list 4
## . .  name = character 1= ggplot2 
## . .  star = integer 1= 1546
## . .  maintainer = character 1= hadley 
## . .  authors = character 1= hadley 
## .  [[2]] = list 4
## . .  name = character 1= knitr 
## . .  star = integer 1= 1047
## . .  maintainer = character 1= yihui 
## . .  authors = character 3= yihui hadley  ... 
## .  [[3]] = list 4
## . .  name = character 1= dplyr 
## . .  star = integer 1= 979
## . .  maintainer = character 1= hadley 
## . .  authors = character 2= hadley romain

packages %>% purrr::sort_by('star') %>% rev()
# 略

order のように結果をインデックスで得るためには rlist::list.order もしくは purrr::order_by

rlist::list.order(packages, star)
## [1] 1 3 2

purrr::order_by(packages, 'star')
## [1] 1 3 2

まとめ

前回と同じく、{purrr} では map (flatmap)keep (discard) でだいたいのことはできる。処理を少し変えたい場合は 組み込み関数を適当に組み合わせればよいため、追加での学習コストは低いと思う。

一方、{rlist} はパッケージの中だけで処理が完結するので、そちらにメリットを感じる方は {rlist} を使ったほうがよさそう。

さらに続きます。

R言語徹底解説

R言語徹底解説