StatsFragments

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

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

R で関数型プログラミングを行うためのパッケージである {purrr}、すこし使い方がわかってきたので整理をしたい。RStudio のブログの記載をみると、とくにデータ処理フローを関数型のように記述することが目的のようだ。

purrr 0.1.0 | RStudio Blog

The core of purrr is a set of functions for manipulating vectors (atomic vectors, lists, and data frames). The goal is similar to dplyr: help you tackle the most common 90% of data manipulation challenges.

ここでいう"関数型プログラミング言語"とは Haskell のような静的型の言語を想定しており、型チェック、ガード、リフトなど断片的に影響を受けたと思われる関数群をもつ。ただし、同時に Haskell を目指すものではない、とも明言されている。

そもそも R は Scheme の影響をうけているためか、関数型らしい言語仕様やビルトイン関数群 Map, Reduce, Filter などをすでに持っている。ただ、これらの関数群は引数の順序からパイプ演算子と組み合わせて使いにくい。{purrr} でこのあたりが使いやすくなっているとうれしい。

R: Common Higher-Order Functions in Functional Programming...

※ 上記の Map など、S のリファレンスに見当たらなかったため R 独自だと思っていますが、間違っていたら教えてください。

{purrr} によるリストの操作

{purrr} を使うとリストやベクトルの処理が書けるのかをまとめたい。リスト操作のためのパッケージである {rlist} とできること、記法をくらべてみる。

{purrr} を利用する目的は 処理を簡単に、見やすく書くことであるため、{rlist} と比べて可読性が悪いもの、複雑になるものは "できない" 処理として扱う。サンプルデータとして、R の著名パッケージに関連した情報のリストを用意した。

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

1レベル目がレコード、以降のレベルが各レコードの要素となっている。この記事では主に以下二つの操作に関連した機能について記載する。

  • 要素に対する操作: 特定の要素を選択する。特定の要素を元に新しい要素をつくる (マッピングする)。
  • レコードに対する操作: 特定のレコードを選択する。

準備: リストの表示

既定の print ではリストの階層構造がわかりにくく、出力も長い。わかりやすくするため Hmisc::list.tree を利用した出力を記載する。また、出力が同一のものは省略する。

library(Hmisc)
l1 <- list(a = 1, b = c(3L, 4L), c = list(x = c(5, 6), y = 'AAA'))

# 既定での表示
l1
## $a
## [1] 1
## 
## $b
## [1] 3 4
## 
## $c
## $c$x
## [1] 5 6
## 
## $c$y
## [1] "AAA"

# list.tree での表示 (名前 = 型 要素数 = 値 と読む)
Hmisc::list.tree(l1)
## l1 = list 3 (968 bytes)
## .  a = double 1= 1
## .  b = integer 2= 3 4
## .  c = list 2
## . .  x = double 2= 5 6
## . .  y = character 1= AAA 

パッケージのロード

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

要素の選択とマッピング

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

要素の選択

R 標準の lapplyapply 系の関数に対応するのは rlist::list.mappurrr::mappurrr::map では第二引数に文字列を渡すとその要素の抽出、ラムダ式 ( ~ .$name )を渡すとその式の適用になる。

lapply(packages, function(x) { x$name })
##  x = list 3 (360 bytes)
## .  [[1]] = character 1= dplyr 
## .  [[2]] = character 1= ggplot2 
## .  [[3]] = character 1= knitr

rlist::list.map(packages, name)
# 略

purrr::map(packages, 'name')
# 略

purrr::map(packages, ~ .$name)
# 略

複数の要素を一度に選択したい場合、{rlist} には rlist::list.select という選択専用の関数がある。{purrr} では渡せる式が一つのため、ラムダ式を利用する。

rlist::list.select(packages, maintainer, star)
##  x = list 3 (1488 bytes)
## .  [[1]] = list 2
## . .  maintainer = character 1= hadley 
## . .  star = integer 1= 979
## .  [[2]] = list 2
## . .  maintainer = character 1= hadley 
## . .  star = integer 1= 1546
## .  [[3]] = list 2
## . .  maintainer = character 1= yihui 
## . .  star = integer 1= 1047

purrr::map(packages, ~ .[c('maintainer', 'star')])
# 略

補足 第二引数にベクトルを渡した場合の処理は、以下のように階層的な選択になる ( l[[c('a', 'b')]] と一緒)。

l2 <- list(list(a=list(b=1)), list(a=list(b=2)))
l2
##  x = list 2 (1176 bytes)
## .  [[1]] = list 1
## . .  a = list 1
## . . .  b = double 1= 1
## .  [[2]] = list 1
## . .  a = list 1
## . . .  b = double 1= 2

purrr::map(l2, c('a', 'b'))
##  x = list 2 (152 bytes)
## .  [[1]] = double 1= 1
## .  [[2]] = double 1= 2

{rlist}, {purrr} でのラムダ式

f <- function(.) {. + 1} に対応する無名関数を {rlist}, {purrr} それぞれの記法で書く。形式はチルダの有無以外は同じ。ドットが引数に対応する。

nums <- c(a = 3, b = 2, c = 1)
rlist::list.map(nums, . + 1)
##  x = list 3 (544 bytes)
## .  a = double 1= 4
## .  b = double 1= 3
## .  c = double 1= 2

purrr::map(nums, ~ . + 1)
# 略

{rilst} では ラムダ式中の .i で元のリストの位置 (インデックス) を、.name で名前 (names) をそれぞれ参照できる。purrrラムダ式には対応する記法がないため、近いことをやるためには直接 map の引数として渡す必要がある。

rlist::list.map(nums, .i) 
##  x = list 3 (544 bytes)
## .  a = integer 1= 1
## .  b = integer 1= 2
## .  c = integer 1= 3

purrr::map(seq_along(nums), ~ .)  # namesは変わってしまう
##  x = list 3 (216 bytes)
## .  [[1]] = integer 1= 1
## .  [[2]] = integer 1= 2
## .  [[3]] = integer 1= 3
rlist::list.map(nums, paste0("Name: ", .name))
##  x = list 3 (688 bytes)
## .  a = character 1= Name: a 
## .  b = character 1= Name: b 
## .  c = character 1= Name: c

purrr::map(names(nums), ~ paste0("Name: ", .)) # namesは変わってしまう
##  x = list 3 (360 bytes)
## .  [[1]] = character 1= Name: a 
## .  [[2]] = character 1= Name: b 
## .  [[3]] = character 1= Name: c

また、内部的にはラムダ式eval で評価をしているため、変数として扱われない位置にあるドットは評価されない。

purrr::map(nums, ~ list(. = 1))
##  x = list 3 (1312 bytes)
## .  a = list 1
## . .  . = double 1= 1
## .  b = list 1
## . .  . = double 1= 1
## .  c = list 1
## . .  . = double 1= 1

要素の追加、変更

元となるリストの値から新しいリストを作りたい場合はラムダ式でリストを返す。

rlist::list.map(packages, list(star = star, had = 'hadley' %in% authors))
##  x = list 3 (1320 bytes)
## .  [[1]] = list 2
## . .  star = integer 1= 979
## . .  had = logical 1= TRUE
## .  [[2]] = list 2
## . .  star = integer 1= 1546
## . .  had = logical 1= TRUE
## .  [[3]] = list 2
## . .  star = integer 1= 1047
## . .  had = logical 1= TRUE

purrr::map(packages, ~ list(star = .$star, had = 'hadley' %in% .$authors))
# 略

結果の型の変更

結果をベクトルで取得したい場合、{rilst} では list.mapv{purrr} では flatmap もしくは map_int ( map_xxx のように結果の型を指定できる関数群がある )。

rlist::list.mapv(packages, star)
## [1]  979 1546 1047

purrr::flatmap(packages, 'star')
# 略

purrr::map_int(packages, 'star')
# 略

data.frame としたい場合は rlist::list.stack{purrr} では dplyr::bind_rows に渡す。

packages %>>%
  rlist::list.select(name, star) %>>%
  rlist::list.stack()
##      name star
## 1   dplyr  979
## 2 ggplot2 1546
## 3   knitr 1047

packages %>%
  purrr::map(~ .[c('name', 'star')]) %>%
  dplyr::bind_rows())
# 略

関数の適用

返り値を変更せずに関数を適用するには rlist::list.iterpurrr::walk。パイプ処理の間に出力やプロットなど意図する返り値を持たない処理を挟み込む場合に利用する。

r <- rlist::list.iter(packages, cat(name, ":", star, "\n"))
## dplyr : 979 
## ggplot2 : 1546 
## knitr : 1047

r <- purrr::walk(packages, ~ cat(.$name, ":", .$star, "\n"))
# 略

レコードの選択

rlist Tutorial: Filtering 前半の単純な条件での選択。

{rilst} では list.filter{purrr} では keepラムダ式が使えるのは map などと同じ。

packages %>>%
  list.filter(star >= 1500)
##  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 >= 1500)
# 略
packages %>>%
  list.filter("yihui" %in% authors)
##  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  ...

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

まとめ

{purrr} によるリストデータの属性、レコードに対する操作を記載した。purrr::mappurrr::keep だけでもパイプ演算子 + ラムダ式と組み合わせて幅広い処理ができそうだ。

11/28追記 続きです。

sinhrks.hatenablog.com

R言語徹底解説

R言語徹底解説